fix(channels): normalize OpenClaw channel config policies

This commit is contained in:
晴天
2026-05-23 01:14:42 +08:00
parent f4d644ea06
commit 27b35b6298
5 changed files with 904 additions and 84 deletions

View File

@@ -2386,6 +2386,219 @@ function platformBindingChannel(platform) {
return platformListId(storageKey)
}
function csvToStringArray(raw) {
if (Array.isArray(raw)) return raw.map(item => String(item).trim()).filter(Boolean)
if (typeof raw !== 'string') return []
return raw.split(/[,;\n]/).map(item => item.trim()).filter(Boolean)
}
function normalizeDmPolicy(raw, fallback = 'pairing') {
const value = String(raw || '').trim()
if (!value) return fallback
if (value === 'allow') return 'open'
if (value === 'deny') return 'disabled'
if (['pairing', 'allowlist', 'open', 'disabled'].includes(value)) return value
return fallback
}
function normalizeGroupPolicy(raw, fallback = 'allowlist') {
const value = String(raw || '').trim()
if (!value) return fallback
if (value === 'all') return 'open'
if (value === 'mentioned') return 'open'
if (value === 'deny') return 'disabled'
if (['open', 'allowlist', 'disabled'].includes(value)) return value
return fallback
}
function putWildcardAllowFromWhenOpen(entry, previousAllowFrom) {
if (entry.dmPolicy !== 'open') return
const allowFrom = csvToStringArray(previousAllowFrom)
if (!allowFrom.includes('*')) allowFrom.push('*')
entry.allowFrom = allowFrom
}
function platformSupportsTopLevelRequireMention(platform) {
return ['feishu', 'slack', 'msteams'].includes(platformStorageKey(platform))
}
export function normalizeMessagingPlatformForm(platform, form = {}) {
const storageKey = platformStorageKey(platform)
const normalized = { ...(form || {}) }
if (!Object.hasOwn(normalized, 'allowFrom') && Object.hasOwn(normalized, 'allowedUsers')) {
normalized.allowFrom = normalized.allowedUsers
}
const needsAccessDefaults = ['telegram', 'discord', 'feishu', 'slack', 'signal', 'msteams', 'whatsapp'].includes(storageKey)
const hasDmField = Object.hasOwn(normalized, 'dmPolicy') || needsAccessDefaults
const hasGroupField = Object.hasOwn(normalized, 'groupPolicy') || needsAccessDefaults
if (hasDmField) {
normalized.dmPolicy = normalizeDmPolicy(normalized.dmPolicy)
if (Object.hasOwn(normalized, 'allowFrom')) normalized.allowFrom = csvToStringArray(normalized.allowFrom)
putWildcardAllowFromWhenOpen(normalized, normalized.allowFrom)
} else if (Object.hasOwn(normalized, 'allowFrom')) {
normalized.allowFrom = csvToStringArray(normalized.allowFrom)
}
if (hasGroupField) {
const requestedGroupPolicy = String(normalized.groupPolicy || '').trim()
normalized.groupPolicy = normalizeGroupPolicy(requestedGroupPolicy)
if (requestedGroupPolicy === 'mentioned' && platformSupportsTopLevelRequireMention(storageKey)) {
normalized.requireMention = true
} else if (requestedGroupPolicy !== 'mentioned') {
if (platformSupportsTopLevelRequireMention(storageKey)) {
normalized.requireMention = false
} else if (Object.hasOwn(normalized, 'requireMention')) {
normalized.requireMention = normalized.requireMention === true || normalized.requireMention === 'true'
}
}
}
if (storageKey === 'feishu') {
normalized.domain = String(normalized.domain || '').trim() || 'feishu'
normalized.connectionMode = normalized.connectionMode || 'websocket'
normalized.webhookPath = normalized.webhookPath || '/feishu/events'
normalized.reactionNotifications = normalized.reactionNotifications || 'off'
if (!Object.hasOwn(normalized, 'typingIndicator')) normalized.typingIndicator = true
if (!Object.hasOwn(normalized, 'resolveSenderNames')) normalized.resolveSenderNames = true
}
if (storageKey === 'slack') {
normalized.mode = normalized.mode || 'socket'
normalized.webhookPath = normalized.webhookPath || '/slack/events'
if (!Object.hasOwn(normalized, 'userTokenReadOnly')) normalized.userTokenReadOnly = false
}
return normalized
}
function csvForForm(raw) {
return csvToStringArray(raw).join(', ')
}
function putStringFormValue(form, source, key) {
if (typeof source?.[key] === 'string') form[key] = source[key]
}
function putBoolFormValue(form, source, key) {
if (typeof source?.[key] === 'boolean') form[key] = source[key] ? 'true' : 'false'
}
function putCsvFormValue(form, source, key) {
const value = csvForForm(source?.[key])
if (value) form[key] = value
}
function putAccessPolicyFormValues(form, source, { telegramCompat = false, mentionCompat = false } = {}) {
putStringFormValue(form, source, 'dmPolicy')
putStringFormValue(form, source, 'groupPolicy')
if (mentionCompat && form.groupPolicy === 'open' && source?.requireMention === true) {
form.groupPolicy = 'mentioned'
}
putCsvFormValue(form, source, 'allowFrom')
if (telegramCompat && form.allowFrom) form.allowedUsers = form.allowFrom
}
export function buildMessagingPlatformFormValues(platform, saved = {}, options = {}) {
if (!saved || typeof saved !== 'object') return {}
const form = {}
const storageKey = platformStorageKey(platform)
if (storageKey === 'telegram') {
putStringFormValue(form, saved, 'botToken')
putAccessPolicyFormValues(form, saved, { telegramCompat: true })
return form
}
if (storageKey === 'discord') {
putStringFormValue(form, saved, 'token')
putAccessPolicyFormValues(form, saved)
const guilds = saved.guilds && typeof saved.guilds === 'object' ? saved.guilds : null
const guildId = guilds ? Object.keys(guilds)[0] : ''
if (guildId) {
form.guildId = guildId
const channels = guilds[guildId]?.channels && typeof guilds[guildId].channels === 'object'
? guilds[guildId].channels
: null
const channelId = channels ? Object.keys(channels).find(id => id !== '*') : ''
if (channelId) form.channelId = channelId
}
return form
}
if (storageKey === 'feishu') {
putStringFormValue(form, saved, 'appId')
putStringFormValue(form, saved, 'appSecret')
const shared = options.channelRoot && typeof options.channelRoot === 'object'
? { ...saved, ...options.channelRoot }
: saved
for (const key of ['domain', 'connectionMode', 'webhookPath', 'reactionNotifications', 'textChunkLimit', 'mediaMaxMb']) {
putStringFormValue(form, shared, key)
}
putAccessPolicyFormValues(form, shared, { mentionCompat: true })
putBoolFormValue(form, shared, 'typingIndicator')
putBoolFormValue(form, shared, 'resolveSenderNames')
putBoolFormValue(form, shared, 'requireMention')
return form
}
if (storageKey === 'slack') {
for (const key of ['mode', 'botToken', 'appToken', 'signingSecret', 'webhookPath', 'teamId', 'appId', 'socketMode']) {
putStringFormValue(form, saved, key)
}
putAccessPolicyFormValues(form, saved, { mentionCompat: true })
putBoolFormValue(form, saved, 'userTokenReadOnly')
putBoolFormValue(form, saved, 'requireMention')
return form
}
if (storageKey === 'whatsapp') {
putAccessPolicyFormValues(form, saved, { mentionCompat: true })
putBoolFormValue(form, saved, 'enabled')
return form
}
if (storageKey === 'signal') {
for (const key of ['account', 'cliPath', 'httpUrl', 'httpHost', 'httpPort']) {
putStringFormValue(form, saved, key)
}
putAccessPolicyFormValues(form, saved)
return form
}
if (storageKey === 'matrix') {
for (const key of ['homeserver', 'accessToken', 'userId', 'password', 'deviceId']) {
putStringFormValue(form, saved, key)
}
putAccessPolicyFormValues(form, saved)
putBoolFormValue(form, saved, 'e2ee')
if (form.accessToken) form.authMode = 'token'
else if (form.userId || form.password) form.authMode = 'password'
return form
}
if (storageKey === 'msteams') {
for (const key of ['appId', 'appPassword', 'tenantId', 'botEndpoint', 'webhookPath']) {
putStringFormValue(form, saved, key)
}
putAccessPolicyFormValues(form, saved)
putBoolFormValue(form, saved, 'requireMention')
return form
}
for (const [key, value] of Object.entries(saved)) {
if (key === 'enabled' || key === 'accounts') continue
if (typeof value === 'string') form[key] = value
else if (Array.isArray(value)) {
const csv = csvForForm(value)
if (csv) form[key] = csv
} else if (typeof value === 'boolean') {
form[key] = value ? 'true' : 'false'
}
}
return form
}
function channelHasQqbotCredentials(entry) {
return !!(entry && typeof entry === 'object' && (entry.appId || entry.clientSecret || entry.appSecret || entry.token))
}
@@ -3872,21 +4085,8 @@ const handlers = {
if (!appId && !clientSecret) return { exists: false }
if (appId) form.appId = appId
if (clientSecret) form.clientSecret = clientSecret
} else if (platform === 'telegram') {
if (saved.botToken) form.botToken = saved.botToken
if (saved.allowFrom) form.allowedUsers = saved.allowFrom.join(', ')
} else if (platform === 'discord') {
if (saved.token) form.token = saved.token
const gid = saved.guilds && Object.keys(saved.guilds)[0]
if (gid) form.guildId = gid
} else if (platform === 'feishu') {
if (saved.appId) form.appId = saved.appId
if (saved.appSecret) form.appSecret = saved.appSecret
if (saved.domain) form.domain = saved.domain
} else {
for (const [k, v] of Object.entries(saved)) {
if (k !== 'enabled' && k !== 'accounts' && typeof v === 'string') form[k] = v
}
Object.assign(form, buildMessagingPlatformFormValues(platform, saved, { channelRoot }))
}
return { exists: true, values: form }
},
@@ -3894,6 +4094,7 @@ const handlers = {
save_messaging_platform({ platform, form, accountId }) {
if (!fs.existsSync(CONFIG_PATH)) throw new Error('openclaw.json 不存在')
const cfg = readOpenclawConfigRequired()
form = normalizeMessagingPlatformForm(platform, form || {})
if (!cfg.channels) cfg.channels = {}
const storageKey = platformStorageKey(platform)
const normalizedAccountId = typeof accountId === 'string' ? accountId.trim() : ''
@@ -3936,9 +4137,14 @@ const handlers = {
cfg.channels.qqbot = current
} else if (platform === 'telegram') {
entry.botToken = form.botToken
if (form.allowedUsers) entry.allowFrom = form.allowedUsers.split(',').map(s => s.trim()).filter(Boolean)
entry.dmPolicy = form.dmPolicy
entry.groupPolicy = form.groupPolicy
if (Array.isArray(form.allowFrom) && form.allowFrom.length) entry.allowFrom = form.allowFrom
} else if (platform === 'discord') {
entry.token = form.token
entry.dmPolicy = form.dmPolicy
entry.groupPolicy = form.groupPolicy
if (Array.isArray(form.allowFrom) && form.allowFrom.length) entry.allowFrom = form.allowFrom
if (form.guildId) {
const ck = form.channelId || '*'
entry.guilds = { [form.guildId]: { users: ['*'], requireMention: true, channels: { [ck]: { allow: true, requireMention: true } } } }
@@ -3947,7 +4153,15 @@ const handlers = {
entry.appId = form.appId
entry.appSecret = form.appSecret
entry.connectionMode = 'websocket'
if (form.domain) entry.domain = form.domain
entry.domain = form.domain
entry.webhookPath = form.webhookPath
entry.dmPolicy = form.dmPolicy
entry.groupPolicy = form.groupPolicy
if (Array.isArray(form.allowFrom) && form.allowFrom.length) entry.allowFrom = form.allowFrom
if (Object.hasOwn(form, 'requireMention')) entry.requireMention = !!form.requireMention
entry.reactionNotifications = form.reactionNotifications
entry.typingIndicator = form.typingIndicator
entry.resolveSenderNames = form.resolveSenderNames
if (normalizedAccountId) {
setAccountChannelEntry(entry)
} else {

View File

@@ -80,6 +80,28 @@ fn insert_array_as_csv(form: &mut Map<String, Value>, source: &Value, key: &str)
}
}
fn insert_access_policy_form_values(
form: &mut Map<String, Value>,
source: &Value,
telegram_compat: bool,
mention_compat: bool,
) {
insert_string_if_present(form, source, "dmPolicy");
insert_string_if_present(form, source, "groupPolicy");
if mention_compat
&& form.get("groupPolicy").and_then(|v| v.as_str()) == Some("open")
&& source.get("requireMention").and_then(|v| v.as_bool()) == Some(true)
{
form.insert("groupPolicy".into(), Value::String("mentioned".into()));
}
insert_array_as_csv(form, source, "allowFrom");
if telegram_compat {
if let Some(v) = form.get("allowFrom").cloned() {
form.insert("allowedUsers".into(), v);
}
}
}
fn csv_to_json_array(raw: &str) -> Option<Value> {
let items = raw
.split(&[',', '\n', ';'][..])
@@ -94,6 +116,32 @@ fn csv_to_json_array(raw: &str) -> Option<Value> {
}
}
fn json_array_from_csv_value(value: Option<&Value>) -> Vec<Value> {
match value {
Some(Value::Array(items)) => items
.iter()
.filter_map(|v| {
if let Some(s) = v.as_str() {
let trimmed = s.trim();
if trimmed.is_empty() {
None
} else {
Some(Value::String(trimmed.to_string()))
}
} else if v.is_number() || v.is_boolean() {
Some(Value::String(v.to_string()))
} else {
None
}
})
.collect(),
Some(Value::String(raw)) => csv_to_json_array(raw)
.and_then(|v| v.as_array().cloned())
.unwrap_or_default(),
_ => vec![],
}
}
fn bool_from_form_value(raw: &str) -> Option<bool> {
match raw.trim().to_ascii_lowercase().as_str() {
"true" | "1" | "yes" | "on" => Some(true),
@@ -114,12 +162,161 @@ fn put_bool_from_form(entry: &mut Map<String, Value>, key: &str, raw: &str) {
}
}
fn put_csv_array_from_form(entry: &mut Map<String, Value>, key: &str, raw: &str) {
if let Some(v) = csv_to_json_array(raw) {
entry.insert(key.into(), v);
fn put_bool_value_if_present(entry: &mut Map<String, Value>, key: &str, value: Option<&Value>) {
match value {
Some(Value::Bool(v)) => {
entry.insert(key.into(), Value::Bool(*v));
}
Some(Value::String(raw)) => put_bool_from_form(entry, key, raw),
_ => {}
}
}
fn put_array_from_form_value(entry: &mut Map<String, Value>, key: &str, value: Option<&Value>) {
let items = json_array_from_csv_value(value);
if !items.is_empty() {
entry.insert(key.into(), Value::Array(items));
}
}
fn normalize_dm_policy_value(raw: Option<&Value>, fallback: &str) -> String {
let value = raw.and_then(|v| v.as_str()).unwrap_or("").trim();
match value {
"" => fallback.to_string(),
"allow" | "open" => "open".into(),
"deny" | "disabled" => "disabled".into(),
"pairing" => "pairing".into(),
"allowlist" => "allowlist".into(),
_ => fallback.to_string(),
}
}
fn normalize_group_policy_value(raw: Option<&Value>, fallback: &str) -> String {
let value = raw.and_then(|v| v.as_str()).unwrap_or("").trim();
match value {
"" => fallback.to_string(),
"all" | "mentioned" | "open" => "open".into(),
"deny" | "disabled" => "disabled".into(),
"allowlist" => "allowlist".into(),
_ => fallback.to_string(),
}
}
fn platform_supports_top_level_require_mention(platform: &str) -> bool {
matches!(
platform_storage_key(platform),
"feishu" | "slack" | "msteams"
)
}
fn normalize_messaging_platform_form(
platform: &str,
form: &Map<String, Value>,
) -> Map<String, Value> {
let storage_key = platform_storage_key(platform);
let mut normalized = form.clone();
if !normalized.contains_key("allowFrom") {
if let Some(v) = normalized.get("allowedUsers").cloned() {
normalized.insert("allowFrom".into(), v);
}
}
let needs_access_defaults = matches!(
storage_key,
"telegram" | "discord" | "feishu" | "slack" | "signal" | "msteams" | "whatsapp"
);
let has_dm_field = normalized.contains_key("dmPolicy") || needs_access_defaults;
let has_group_field = normalized.contains_key("groupPolicy") || needs_access_defaults;
if has_dm_field {
let dm_policy = normalize_dm_policy_value(normalized.get("dmPolicy"), "pairing");
normalized.insert("dmPolicy".into(), Value::String(dm_policy.clone()));
if normalized.contains_key("allowFrom") {
let items = json_array_from_csv_value(normalized.get("allowFrom"));
normalized.insert("allowFrom".into(), Value::Array(items));
}
if dm_policy == "open" {
let mut items = json_array_from_csv_value(normalized.get("allowFrom"));
if !items.iter().any(|v| v.as_str() == Some("*")) {
items.push(Value::String("*".into()));
}
normalized.insert("allowFrom".into(), Value::Array(items));
}
} else if normalized.contains_key("allowFrom") {
let items = json_array_from_csv_value(normalized.get("allowFrom"));
normalized.insert("allowFrom".into(), Value::Array(items));
}
if has_group_field {
let requested_group_policy = normalized
.get("groupPolicy")
.and_then(|v| v.as_str())
.unwrap_or("")
.trim()
.to_string();
let group_policy = normalize_group_policy_value(normalized.get("groupPolicy"), "allowlist");
normalized.insert("groupPolicy".into(), Value::String(group_policy));
if requested_group_policy == "mentioned"
&& platform_supports_top_level_require_mention(storage_key)
{
normalized.insert("requireMention".into(), Value::Bool(true));
} else if requested_group_policy != "mentioned" {
if platform_supports_top_level_require_mention(storage_key) {
normalized.insert("requireMention".into(), Value::Bool(false));
} else if normalized.contains_key("requireMention") {
let value = match normalized.get("requireMention") {
Some(Value::Bool(v)) => *v,
Some(Value::String(s)) => bool_from_form_value(s).unwrap_or(false),
_ => false,
};
normalized.insert("requireMention".into(), Value::Bool(value));
}
}
}
if storage_key == "feishu" {
let domain = normalized
.get("domain")
.and_then(|v| v.as_str())
.unwrap_or("")
.trim();
normalized.insert(
"domain".into(),
Value::String(if domain.is_empty() { "feishu" } else { domain }.into()),
);
normalized
.entry("connectionMode")
.or_insert(Value::String("websocket".into()));
normalized
.entry("webhookPath")
.or_insert(Value::String("/feishu/events".into()));
normalized
.entry("reactionNotifications")
.or_insert(Value::String("off".into()));
normalized
.entry("typingIndicator")
.or_insert(Value::Bool(true));
normalized
.entry("resolveSenderNames")
.or_insert(Value::Bool(true));
}
if storage_key == "slack" {
normalized
.entry("mode")
.or_insert(Value::String("socket".into()));
normalized
.entry("webhookPath")
.or_insert(Value::String("/slack/events".into()));
normalized
.entry("userTokenReadOnly")
.or_insert(Value::Bool(false));
}
normalized
}
/// 合并渠道配置:将新的表单字段覆盖到现有配置上,保留用户通过 CLI 或手动编辑的自定义字段。
/// 例如用户手动添加的 streaming / retry / dmPolicy 等不会被丢弃。
fn merge_channel_entry(
@@ -327,6 +524,7 @@ pub async fn read_platform_config(
if let Some(t) = saved.get("token").and_then(|v| v.as_str()) {
form.insert("token".into(), Value::String(t.into()));
}
insert_access_policy_form_values(&mut form, &saved, false, false);
if let Some(guilds) = saved.get("guilds").and_then(|v| v.as_object()) {
if let Some(gid) = guilds.keys().next() {
form.insert("guildId".into(), Value::String(gid.clone()));
@@ -349,10 +547,7 @@ pub async fn read_platform_config(
if let Some(t) = saved.get("botToken").and_then(|v| v.as_str()) {
form.insert("botToken".into(), Value::String(t.into()));
}
if let Some(arr) = saved.get("allowFrom").and_then(|v| v.as_array()) {
let users: Vec<&str> = arr.iter().filter_map(|v| v.as_str()).collect();
form.insert("allowedUsers".into(), Value::String(users.join(", ")));
}
insert_access_policy_form_values(&mut form, &saved, true, false);
}
"qqbot" => {
// 多账号:读 accounts.<account_id>;单账号:先读 qqbot 根节点,若无凭证再读 accounts.default与官方 CLI 一致)
@@ -475,34 +670,72 @@ pub async fn read_platform_config(
if let Some(ref acct) = account_id {
if !acct.is_empty() {
// 从 channel root 补 shared fields
let mut shared_source = saved.clone();
if let Some(ch_root) = channel_root {
if let (Some(target), Some(root)) =
(shared_source.as_object_mut(), ch_root.as_object())
{
for key in &[
"domain",
"connectionMode",
"webhookPath",
"dmPolicy",
"groupPolicy",
"allowFrom",
"reactionNotifications",
"typingIndicator",
"resolveSenderNames",
"requireMention",
"textChunkLimit",
"mediaMaxMb",
] {
if let Some(v) = root.get(*key) {
target.insert(key.to_string(), v.clone());
}
}
}
}
{
for key in &[
"domain",
"connectionMode",
"dmPolicy",
"groupPolicy",
"webhookPath",
"groupAllowFrom",
"groups",
"reactionNotifications",
"streaming",
"blockStreaming",
"typingIndicator",
"resolveSenderNames",
"textChunkLimit",
"mediaMaxMb",
] {
if let Some(v) = ch_root.get(*key) {
if let Some(v) = shared_source.get(*key) {
if !v.is_null() {
form.insert(key.to_string(), v.clone());
}
}
}
insert_access_policy_form_values(&mut form, &shared_source, false, true);
insert_bool_as_string(&mut form, &shared_source, "typingIndicator");
insert_bool_as_string(&mut form, &shared_source, "resolveSenderNames");
insert_bool_as_string(&mut form, &shared_source, "requireMention");
}
}
} else {
// 无账号:直接从 root 读 shared fields
if let Some(v) = saved.get("domain").and_then(|v| v.as_str()) {
form.insert("domain".into(), Value::String(v.into()));
for key in &[
"domain",
"connectionMode",
"webhookPath",
"reactionNotifications",
"textChunkLimit",
"mediaMaxMb",
] {
insert_string_if_present(&mut form, &saved, key);
}
insert_access_policy_form_values(&mut form, &saved, false, true);
insert_bool_as_string(&mut form, &saved, "typingIndicator");
insert_bool_as_string(&mut form, &saved, "resolveSenderNames");
insert_bool_as_string(&mut form, &saved, "requireMention");
}
}
"dingtalk" | "dingtalk-connector" => {
@@ -543,14 +776,12 @@ pub async fn read_platform_config(
insert_string_if_present(&mut form, &saved, "teamId");
insert_string_if_present(&mut form, &saved, "appId");
insert_string_if_present(&mut form, &saved, "socketMode");
insert_string_if_present(&mut form, &saved, "dmPolicy");
insert_string_if_present(&mut form, &saved, "groupPolicy");
insert_array_as_csv(&mut form, &saved, "allowFrom");
insert_access_policy_form_values(&mut form, &saved, false, true);
insert_bool_as_string(&mut form, &saved, "userTokenReadOnly");
insert_bool_as_string(&mut form, &saved, "requireMention");
}
"whatsapp" => {
insert_string_if_present(&mut form, &saved, "dmPolicy");
insert_string_if_present(&mut form, &saved, "groupPolicy");
insert_array_as_csv(&mut form, &saved, "allowFrom");
insert_access_policy_form_values(&mut form, &saved, false, false);
insert_bool_as_string(&mut form, &saved, "enabled");
}
"signal" => {
@@ -559,9 +790,7 @@ pub async fn read_platform_config(
insert_string_if_present(&mut form, &saved, "httpUrl");
insert_string_if_present(&mut form, &saved, "httpHost");
insert_string_if_present(&mut form, &saved, "httpPort");
insert_string_if_present(&mut form, &saved, "dmPolicy");
insert_string_if_present(&mut form, &saved, "groupPolicy");
insert_array_as_csv(&mut form, &saved, "allowFrom");
insert_access_policy_form_values(&mut form, &saved, false, false);
}
"matrix" => {
insert_string_if_present(&mut form, &saved, "homeserver");
@@ -569,10 +798,8 @@ pub async fn read_platform_config(
insert_string_if_present(&mut form, &saved, "userId");
insert_string_if_present(&mut form, &saved, "password");
insert_string_if_present(&mut form, &saved, "deviceId");
insert_string_if_present(&mut form, &saved, "dmPolicy");
insert_string_if_present(&mut form, &saved, "groupPolicy");
insert_access_policy_form_values(&mut form, &saved, false, false);
insert_bool_as_string(&mut form, &saved, "e2ee");
insert_array_as_csv(&mut form, &saved, "allowFrom");
if saved.get("accessToken").and_then(|v| v.as_str()).is_some() {
form.insert("authMode".into(), Value::String("token".into()));
} else if saved.get("userId").and_then(|v| v.as_str()).is_some()
@@ -587,9 +814,8 @@ pub async fn read_platform_config(
insert_string_if_present(&mut form, &saved, "tenantId");
insert_string_if_present(&mut form, &saved, "botEndpoint");
insert_string_if_present(&mut form, &saved, "webhookPath");
insert_string_if_present(&mut form, &saved, "dmPolicy");
insert_string_if_present(&mut form, &saved, "groupPolicy");
insert_array_as_csv(&mut form, &saved, "allowFrom");
insert_access_policy_form_values(&mut form, &saved, false, true);
insert_bool_as_string(&mut form, &saved, "requireMention");
}
_ => {
if saved.is_null() {
@@ -641,7 +867,9 @@ pub async fn save_messaging_platform(
.or_insert_with(|| json!({}));
let channels_map = channels.as_object_mut().ok_or("channels 节点格式错误")?;
let form_obj = form.as_object().ok_or("表单数据格式错误")?;
let raw_form_obj = form.as_object().ok_or("表单数据格式错误")?;
let normalized_form = normalize_messaging_platform_form(&platform, raw_form_obj);
let form_obj = &normalized_form;
// 用于后续创建 bindings 的平台信息
let saved_account_id = account_id.clone();
@@ -655,6 +883,13 @@ pub async fn save_messaging_platform(
entry.insert("token".into(), Value::String(t.trim().into()));
}
entry.insert("enabled".into(), Value::Bool(true));
put_string(&mut entry, "dmPolicy", form_string(form_obj, "dmPolicy"));
put_string(
&mut entry,
"groupPolicy",
form_string(form_obj, "groupPolicy"),
);
put_array_from_form_value(&mut entry, "allowFrom", form_obj.get("allowFrom"));
// guildId + channelId 展开为 guilds 嵌套结构
let guild_id = form_obj
@@ -711,19 +946,13 @@ pub async fn save_messaging_platform(
entry.insert("botToken".into(), Value::String(t.trim().into()));
}
entry.insert("enabled".into(), Value::Bool(true));
// allowedUsers 逗号字符串 → allowFrom 数组
if let Some(users_str) = form_obj.get("allowedUsers").and_then(|v| v.as_str()) {
let users: Vec<Value> = users_str
.split(',')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.map(|s| Value::String(s.into()))
.collect();
if !users.is_empty() {
entry.insert("allowFrom".into(), Value::Array(users));
}
}
put_string(&mut entry, "dmPolicy", form_string(form_obj, "dmPolicy"));
put_string(
&mut entry,
"groupPolicy",
form_string(form_obj, "groupPolicy"),
);
put_array_from_form_value(&mut entry, "allowFrom", form_obj.get("allowFrom"));
merge_channel_entry(channels_map, "telegram", entry);
}
@@ -805,17 +1034,40 @@ pub async fn save_messaging_platform(
entry.insert("appId".into(), Value::String(app_id));
entry.insert("appSecret".into(), Value::String(app_secret));
entry.insert("enabled".into(), Value::Bool(true));
entry.insert("connectionMode".into(), Value::String("websocket".into()));
let domain = form_obj
.get("domain")
.and_then(|v| v.as_str())
.unwrap_or("")
.trim()
.to_string();
if !domain.is_empty() {
entry.insert("domain".into(), Value::String(domain));
}
put_string(
&mut entry,
"connectionMode",
form_string(form_obj, "connectionMode"),
);
put_string(&mut entry, "domain", form_string(form_obj, "domain"));
put_string(
&mut entry,
"webhookPath",
form_string(form_obj, "webhookPath"),
);
put_string(&mut entry, "dmPolicy", form_string(form_obj, "dmPolicy"));
put_string(
&mut entry,
"groupPolicy",
form_string(form_obj, "groupPolicy"),
);
put_string(
&mut entry,
"reactionNotifications",
form_string(form_obj, "reactionNotifications"),
);
put_array_from_form_value(&mut entry, "allowFrom", form_obj.get("allowFrom"));
put_bool_value_if_present(
&mut entry,
"typingIndicator",
form_obj.get("typingIndicator"),
);
put_bool_value_if_present(
&mut entry,
"resolveSenderNames",
form_obj.get("resolveSenderNames"),
);
put_bool_value_if_present(&mut entry, "requireMention", form_obj.get("requireMention"));
// 多账号模式:写入 channels.<storage_key>.accounts.<account_id>
if let Some(ref acct) = account_id {
@@ -926,13 +1178,19 @@ pub async fn save_messaging_platform(
);
put_string(&mut entry, "teamId", form_string(form_obj, "teamId"));
put_string(&mut entry, "appId", form_string(form_obj, "appId"));
put_bool_value_if_present(
&mut entry,
"userTokenReadOnly",
form_obj.get("userTokenReadOnly"),
);
put_bool_value_if_present(&mut entry, "requireMention", form_obj.get("requireMention"));
put_string(&mut entry, "dmPolicy", form_string(form_obj, "dmPolicy"));
put_string(
&mut entry,
"groupPolicy",
form_string(form_obj, "groupPolicy"),
);
put_csv_array_from_form(&mut entry, "allowFrom", &form_string(form_obj, "allowFrom"));
put_array_from_form_value(&mut entry, "allowFrom", form_obj.get("allowFrom"));
merge_channel_entry(channels_map, &storage_key, entry);
}
"whatsapp" => {
@@ -944,7 +1202,7 @@ pub async fn save_messaging_platform(
"groupPolicy",
form_string(form_obj, "groupPolicy"),
);
put_csv_array_from_form(&mut entry, "allowFrom", &form_string(form_obj, "allowFrom"));
put_array_from_form_value(&mut entry, "allowFrom", form_obj.get("allowFrom"));
put_bool_from_form(&mut entry, "enabled", &form_string(form_obj, "enabled"));
merge_channel_entry(channels_map, &storage_key, entry);
}
@@ -967,7 +1225,7 @@ pub async fn save_messaging_platform(
"groupPolicy",
form_string(form_obj, "groupPolicy"),
);
put_csv_array_from_form(&mut entry, "allowFrom", &form_string(form_obj, "allowFrom"));
put_array_from_form_value(&mut entry, "allowFrom", form_obj.get("allowFrom"));
merge_channel_entry(channels_map, &storage_key, entry);
}
"matrix" => {
@@ -997,7 +1255,7 @@ pub async fn save_messaging_platform(
form_string(form_obj, "groupPolicy"),
);
put_bool_from_form(&mut entry, "e2ee", &form_string(form_obj, "e2ee"));
put_csv_array_from_form(&mut entry, "allowFrom", &form_string(form_obj, "allowFrom"));
put_array_from_form_value(&mut entry, "allowFrom", form_obj.get("allowFrom"));
merge_channel_entry(channels_map, &storage_key, entry);
ensure_plugin_allowed(&mut cfg, "matrix")?;
}
@@ -1029,7 +1287,8 @@ pub async fn save_messaging_platform(
"groupPolicy",
form_string(form_obj, "groupPolicy"),
);
put_csv_array_from_form(&mut entry, "allowFrom", &form_string(form_obj, "allowFrom"));
put_bool_value_if_present(&mut entry, "requireMention", form_obj.get("requireMention"));
put_array_from_form_value(&mut entry, "allowFrom", form_obj.get("allowFrom"));
merge_channel_entry(channels_map, &storage_key, entry);
ensure_plugin_allowed(&mut cfg, "msteams")?;
}
@@ -3860,3 +4119,159 @@ async fn verify_dingtalk(
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalize_channel_form_adds_telegram_access_defaults() {
let form = json!({
"botToken": "123:token"
});
let normalized =
normalize_messaging_platform_form("telegram", form.as_object().expect("object"));
assert_eq!(
normalized.get("botToken").and_then(|v| v.as_str()),
Some("123:token")
);
assert_eq!(
normalized.get("dmPolicy").and_then(|v| v.as_str()),
Some("pairing")
);
assert_eq!(
normalized.get("groupPolicy").and_then(|v| v.as_str()),
Some("allowlist")
);
}
#[test]
fn normalize_channel_form_converts_legacy_ui_policy_values() {
let form = json!({
"mode": "socket",
"botToken": "xoxb-token",
"appToken": "xapp-token",
"dmPolicy": "allow",
"groupPolicy": "mentioned"
});
let normalized =
normalize_messaging_platform_form("slack", form.as_object().expect("object"));
assert_eq!(
normalized.get("dmPolicy").and_then(|v| v.as_str()),
Some("open")
);
assert_eq!(
normalized
.get("allowFrom")
.and_then(|v| v.as_array())
.cloned(),
Some(vec![Value::String("*".into())])
);
assert_eq!(
normalized.get("groupPolicy").and_then(|v| v.as_str()),
Some("open")
);
assert_eq!(
normalized.get("requireMention").and_then(|v| v.as_bool()),
Some(true)
);
assert_eq!(
normalized.get("webhookPath").and_then(|v| v.as_str()),
Some("/slack/events")
);
assert_eq!(
normalized
.get("userTokenReadOnly")
.and_then(|v| v.as_bool()),
Some(false)
);
}
#[test]
fn normalize_channel_form_avoids_unsupported_top_level_require_mention() {
let form = json!({
"account": "+15551234567",
"dmPolicy": "deny",
"groupPolicy": "mentioned"
});
let normalized =
normalize_messaging_platform_form("signal", form.as_object().expect("object"));
assert_eq!(
normalized.get("dmPolicy").and_then(|v| v.as_str()),
Some("disabled")
);
assert_eq!(
normalized.get("groupPolicy").and_then(|v| v.as_str()),
Some("open")
);
assert!(!normalized.contains_key("requireMention"));
}
#[test]
fn normalize_channel_form_adds_feishu_required_defaults() {
let form = json!({
"appId": "cli_a",
"appSecret": "secret",
"domain": ""
});
let normalized =
normalize_messaging_platform_form("feishu", form.as_object().expect("object"));
assert_eq!(
normalized.get("domain").and_then(|v| v.as_str()),
Some("feishu")
);
assert_eq!(
normalized.get("connectionMode").and_then(|v| v.as_str()),
Some("websocket")
);
assert_eq!(
normalized.get("webhookPath").and_then(|v| v.as_str()),
Some("/feishu/events")
);
assert_eq!(
normalized.get("dmPolicy").and_then(|v| v.as_str()),
Some("pairing")
);
assert_eq!(
normalized.get("groupPolicy").and_then(|v| v.as_str()),
Some("allowlist")
);
assert_eq!(
normalized
.get("reactionNotifications")
.and_then(|v| v.as_str()),
Some("off")
);
assert_eq!(
normalized.get("typingIndicator").and_then(|v| v.as_bool()),
Some(true)
);
assert_eq!(
normalized
.get("resolveSenderNames")
.and_then(|v| v.as_bool()),
Some(true)
);
}
#[test]
fn channel_form_readback_preserves_mention_policy_choice() {
let saved = json!({
"groupPolicy": "open",
"requireMention": true,
"allowFrom": ["U123"]
});
let mut form = Map::new();
insert_access_policy_form_values(&mut form, &saved, false, true);
assert_eq!(
form.get("groupPolicy").and_then(|v| v.as_str()),
Some("mentioned")
);
assert_eq!(form.get("allowFrom").and_then(|v| v.as_str()), Some("U123"));
}
}

View File

@@ -85,10 +85,15 @@ export default {
policyDefault: _('默认', 'Default', '預設'),
dmAllow: _('允许私信', 'Allow DMs', '允許私信'),
dmDeny: _('拒绝私信', 'Deny DMs', '拒絕私信'),
dmPairing: _('配对 / 白名单', 'Pairing / allowlist', '配對 / 白名單'),
dmOpen: _('允许所有私信', 'Allow all DMs', '允許所有私信'),
dmAllowlist: _('仅白名单私信', 'Allowlist only', '僅白名單私信'),
dmDisabled: _('禁用私信', 'Disable DMs', '停用私信'),
groupPolicy: _('群组策略', 'Group Policy', '群組策略'),
groupAllChannels: _('所有频道', 'All channels', '所有頻道'),
groupMentionOnly: _('仅 @提及时', 'Only when @mentioned', '僅 @提及時'),
groupAllowlist: _('白名单', 'Allowlist', '白名單'),
groupDisabled: _('禁用群组', 'Disable groups', '停用群組'),
allowFromPh: _('可选,逗号分隔用户/频道 ID', 'Optional, comma-separated user/channel IDs', '可選,逗號分隔使用者/頻道 ID'),
allowFromHint: _('限制允许的用户或频道 ID留空不限制', 'Restrict to specific user or channel IDs; leave empty for no restriction', '限制允許的使用者或頻道 ID留空不限制'),
weixinLabel: _('微信', 'WeChat'),

View File

@@ -20,6 +20,22 @@ import {
// ── 渠道注册表:面板内置向导,覆盖 OpenClaw 官方渠道 + 国内扩展渠道 ──
const DM_POLICY_OPTIONS = [
{ value: '', label: t('channels.policyDefault') },
{ value: 'pairing', label: t('channels.dmPairing') },
{ value: 'open', label: t('channels.dmOpen') },
{ value: 'allowlist', label: t('channels.dmAllowlist') },
{ value: 'disabled', label: t('channels.dmDisabled') },
]
const GROUP_POLICY_OPTIONS = (allLabel, { mention = false } = {}) => [
{ value: '', label: t('channels.policyDefault') },
{ value: 'open', label: allLabel },
...(mention ? [{ value: 'mentioned', label: t('channels.groupMentionOnly') }] : []),
{ value: 'allowlist', label: t('channels.groupAllowlist') },
{ value: 'disabled', label: t('channels.groupDisabled') },
]
const PLATFORM_REGISTRY = {
qqbot: {
label: t('channels.qqbotLabel'),
@@ -81,11 +97,14 @@ const PLATFORM_REGISTRY = {
{
key: 'domain', label: t('channels.feishuDomainLabel'), type: 'select',
options: [
{ value: '', label: t('channels.feishuDomainFeishu') },
{ value: 'feishu', label: t('channels.feishuDomainFeishu') },
{ value: 'lark', label: t('channels.feishuDomainLark') },
],
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.groupAllGroups'), { mention: true }), required: false },
{ key: 'allowFrom', label: 'Allow From', placeholder: t('channels.allowFromPh'), required: false, hint: t('channels.allowFromHint') },
],
pluginRequired: '@larksuite/openclaw-lark@latest',
pluginId: 'openclaw-lark',
@@ -104,6 +123,9 @@ const PLATFORM_REGISTRY = {
guideFooter: t('channels.telegramGuideFooter'),
fields: [
{ key: 'botToken', label: 'Bot Token', placeholder: '123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11', secret: true, required: true },
{ 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.groupAllGroups')), required: false },
{ key: 'allowFrom', label: 'Allow From', placeholder: t('channels.allowFromPh'), required: false, hint: t('channels.allowFromHint') },
],
configKey: 'telegram',
pairingChannel: 'telegram',
@@ -121,6 +143,9 @@ const PLATFORM_REGISTRY = {
guideFooter: t('channels.discordGuideFooter'),
fields: [
{ key: 'token', label: 'Bot Token', placeholder: 'MTExxxxxxxxx.Gxxxxxx.xxxxxxxx', secret: true, required: true },
{ 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.groupAllChannels')), required: false },
{ key: 'allowFrom', label: 'Allow From', placeholder: t('channels.allowFromPh'), required: false, hint: t('channels.allowFromHint') },
],
configKey: 'discord',
pairingChannel: 'discord',
@@ -150,8 +175,8 @@ const PLATFORM_REGISTRY = {
{ key: 'signingSecret', label: 'Signing Secret', placeholder: t('channels.slackSigningSecretPh'), secret: true, requiredWhen: { mode: 'http' }, hint: t('channels.slackSigningSecretHint') },
{ key: 'teamId', label: 'Team ID', placeholder: t('channels.slackTeamIdPh'), required: false },
{ key: 'webhookPath', label: 'Webhook Path', placeholder: t('channels.slackWebhookPathPh'), required: false },
{ key: 'dmPolicy', label: t('channels.dmPolicy'), type: 'select', options: [{ value: '', label: t('channels.policyDefault') }, { value: 'allow', label: t('channels.dmAllow') }, { value: 'deny', label: t('channels.dmDeny') }], required: false },
{ key: 'groupPolicy', label: t('channels.groupPolicy'), type: 'select', options: [{ value: '', label: t('channels.policyDefault') }, { value: 'all', label: t('channels.groupAllChannels') }, { value: 'mentioned', label: t('channels.groupMentionOnly') }, { value: 'allowlist', label: t('channels.groupAllowlist') }], 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.groupAllChannels'), { mention: true }), required: false },
{ key: 'allowFrom', label: 'Allow From', placeholder: t('channels.allowFromPh'), required: false, hint: t('channels.allowFromHint') },
],
configKey: 'slack',
@@ -196,8 +221,8 @@ const PLATFORM_REGISTRY = {
{ 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: 'dmPolicy', label: t('channels.dmPolicy'), type: 'select', options: [{ value: '', label: t('channels.policyDefault') }, { value: 'allow', label: t('channels.dmAllow') }, { value: 'deny', label: t('channels.dmDeny') }], required: false },
{ key: 'groupPolicy', label: t('channels.groupPolicy'), type: 'select', options: [{ value: '', label: t('channels.policyDefault') }, { value: 'all', label: t('channels.groupAllTeams') }, { value: 'mentioned', label: t('channels.groupMentionOnly') }, { value: 'allowlist', label: t('channels.groupAllowlist') }], 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 },
],
configKey: 'msteams',
@@ -220,8 +245,8 @@ const PLATFORM_REGISTRY = {
{ key: 'httpUrl', label: 'HTTP URL', placeholder: t('channels.optionalEg', { example: 'http://127.0.0.1:8080' }), required: false },
{ key: 'httpHost', label: 'HTTP Host', placeholder: t('channels.optionalEg', { example: '127.0.0.1' }), required: false },
{ key: 'httpPort', label: 'HTTP Port', placeholder: t('channels.optionalEg', { example: '8080' }), required: false },
{ key: 'dmPolicy', label: t('channels.dmPolicy'), type: 'select', options: [{ value: '', label: t('channels.policyDefault') }, { value: 'allow', label: t('channels.dmAllow') }, { value: 'deny', label: t('channels.dmDeny') }], required: false },
{ key: 'groupPolicy', label: t('channels.groupPolicy'), type: 'select', options: [{ value: '', label: t('channels.policyDefault') }, { value: 'all', label: t('channels.groupAllGroups') }, { value: 'mentioned', label: t('channels.groupMentionBot') }, { value: 'allowlist', label: t('channels.groupAllowlist') }], 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.groupAllGroups')), required: false },
{ key: 'allowFrom', label: 'Allow From', placeholder: t('channels.signalAllowFromPh'), required: false },
],
configKey: 'signal',
@@ -243,8 +268,8 @@ const PLATFORM_REGISTRY = {
{ key: 'password', label: 'Password', placeholder: t('channels.matrixPasswordPh'), secret: true, required: false },
{ key: 'deviceId', label: 'Device ID', placeholder: t('channels.optionalEg', { example: 'CLAWPANEL' }), required: false },
{ key: 'e2ee', label: 'E2EE', type: 'select', options: [{ value: '', label: t('channels.policyDefault') }, { value: 'true', label: t('channels.enable') }, { value: 'false', label: t('channels.disable') }], required: false },
{ key: 'dmPolicy', label: t('channels.dmPolicy'), type: 'select', options: [{ value: '', label: t('channels.policyDefault') }, { value: 'allow', label: t('channels.dmAllow') }, { value: 'deny', label: t('channels.dmDeny') }], required: false },
{ key: 'groupPolicy', label: t('channels.groupPolicy'), type: 'select', options: [{ value: '', label: t('channels.policyDefault') }, { value: 'all', label: t('channels.groupAllRooms') }, { value: 'mentioned', label: t('channels.groupMentionBot') }, { value: 'allowlist', label: t('channels.groupAllowlist') }], 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.groupAllRooms')), required: false },
{ key: 'allowFrom', label: 'Allow From', placeholder: t('channels.matrixAllowFromPh'), required: false },
],
configKey: 'matrix',

View File

@@ -0,0 +1,161 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {
buildMessagingPlatformFormValues,
normalizeMessagingPlatformForm,
} from '../scripts/dev-api.js'
test('渠道保存会为 Telegram 补齐新版 OpenClaw 必填访问策略', () => {
const form = normalizeMessagingPlatformForm('telegram', {
botToken: '123:token',
})
assert.equal(form.botToken, '123:token')
assert.equal(form.dmPolicy, 'pairing')
assert.equal(form.groupPolicy, 'allowlist')
})
test('渠道保存会把旧 UI 策略值转换为 OpenClaw 支持的枚举', () => {
const form = normalizeMessagingPlatformForm('slack', {
mode: 'socket',
botToken: 'xoxb-token',
appToken: 'xapp-token',
dmPolicy: 'allow',
groupPolicy: 'mentioned',
})
assert.equal(form.dmPolicy, 'open')
assert.deepEqual(form.allowFrom, ['*'])
assert.equal(form.groupPolicy, 'open')
assert.equal(form.requireMention, true)
assert.equal(form.webhookPath, '/slack/events')
assert.equal(form.userTokenReadOnly, false)
})
test('渠道保存不会向不支持顶层 requireMention 的平台写入非法字段', () => {
const form = normalizeMessagingPlatformForm('signal', {
account: '+15551234567',
dmPolicy: 'deny',
groupPolicy: 'mentioned',
})
assert.equal(form.dmPolicy, 'disabled')
assert.equal(form.groupPolicy, 'open')
assert.equal(Object.hasOwn(form, 'requireMention'), false)
})
test('渠道保存会为飞书补齐新版内核要求的默认字段', () => {
const form = normalizeMessagingPlatformForm('feishu', {
appId: 'cli_a',
appSecret: 'secret',
domain: '',
})
assert.equal(form.domain, 'feishu')
assert.equal(form.connectionMode, 'websocket')
assert.equal(form.webhookPath, '/feishu/events')
assert.equal(form.dmPolicy, 'pairing')
assert.equal(form.groupPolicy, 'allowlist')
assert.equal(form.reactionNotifications, 'off')
assert.equal(form.typingIndicator, true)
assert.equal(form.resolveSenderNames, true)
})
test('渠道读取会把新版访问策略字段回显为表单可编辑值', () => {
const values = buildMessagingPlatformFormValues('telegram', {
botToken: '123:token',
dmPolicy: 'allowlist',
groupPolicy: 'disabled',
allowFrom: ['u-1', 'u-2'],
})
assert.equal(values.botToken, '123:token')
assert.equal(values.dmPolicy, 'allowlist')
assert.equal(values.groupPolicy, 'disabled')
assert.equal(values.allowFrom, 'u-1, u-2')
assert.equal(values.allowedUsers, 'u-1, u-2')
})
test('渠道读取会合并飞书账号凭证和根节点共享策略字段', () => {
const values = buildMessagingPlatformFormValues(
'feishu',
{
appId: 'cli_a',
appSecret: 'secret',
},
{
channelRoot: {
domain: 'lark',
connectionMode: 'websocket',
webhookPath: '/feishu/events',
dmPolicy: 'pairing',
groupPolicy: 'allowlist',
reactionNotifications: 'off',
typingIndicator: true,
resolveSenderNames: false,
},
},
)
assert.equal(values.appId, 'cli_a')
assert.equal(values.appSecret, 'secret')
assert.equal(values.domain, 'lark')
assert.equal(values.connectionMode, 'websocket')
assert.equal(values.webhookPath, '/feishu/events')
assert.equal(values.dmPolicy, 'pairing')
assert.equal(values.groupPolicy, 'allowlist')
assert.equal(values.reactionNotifications, 'off')
assert.equal(values.typingIndicator, 'true')
assert.equal(values.resolveSenderNames, 'false')
})
test('渠道读取飞书多账号时不会用根节点旧凭证覆盖账号凭证', () => {
const values = buildMessagingPlatformFormValues(
'feishu',
{
appId: 'account_app',
appSecret: 'account_secret',
dmPolicy: 'pairing',
},
{
channelRoot: {
appId: 'root_app',
appSecret: 'root_secret',
domain: 'lark',
groupPolicy: 'allowlist',
},
},
)
assert.equal(values.appId, 'account_app')
assert.equal(values.appSecret, 'account_secret')
assert.equal(values.domain, 'lark')
assert.equal(values.dmPolicy, 'pairing')
assert.equal(values.groupPolicy, 'allowlist')
})
test('渠道读取会把 open + requireMention 反向回显为仅提及时策略', () => {
const values = buildMessagingPlatformFormValues('slack', {
mode: 'socket',
botToken: 'xoxb-token',
appToken: 'xapp-token',
groupPolicy: 'open',
requireMention: true,
})
assert.equal(values.groupPolicy, 'mentioned')
assert.equal(values.requireMention, 'true')
})
test('渠道保存会在用户改回所有群组时显式清除仅提及开关', () => {
const form = normalizeMessagingPlatformForm('slack', {
mode: 'socket',
botToken: 'xoxb-token',
appToken: 'xapp-token',
groupPolicy: 'open',
})
assert.equal(form.groupPolicy, 'open')
assert.equal(form.requireMention, false)
})