mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-29 04:10:00 +08:00
feat(channels): add iMessage config compatibility
This commit is contained in:
@@ -2438,7 +2438,7 @@ export function normalizeMessagingPlatformForm(platform, form = {}) {
|
||||
if (!Object.hasOwn(normalized, 'allowFrom') && Object.hasOwn(normalized, 'allowedUsers')) {
|
||||
normalized.allowFrom = normalized.allowedUsers
|
||||
}
|
||||
const needsAccessDefaults = ['telegram', 'discord', 'feishu', 'slack', 'signal', 'msteams', 'whatsapp', 'zalo', 'zalouser', 'line', 'mattermost', 'googlechat'].includes(storageKey)
|
||||
const needsAccessDefaults = ['telegram', 'discord', 'feishu', 'slack', 'signal', 'msteams', 'whatsapp', 'zalo', 'zalouser', 'line', 'mattermost', 'googlechat', 'imessage'].includes(storageKey)
|
||||
const hasDmField = Object.hasOwn(normalized, 'dmPolicy') || needsAccessDefaults
|
||||
const hasGroupField = Object.hasOwn(normalized, 'groupPolicy') || needsAccessDefaults
|
||||
|
||||
@@ -2472,11 +2472,11 @@ export function normalizeMessagingPlatformForm(platform, form = {}) {
|
||||
normalized.allowedUserIds = csvToStringArray(normalized.allowedUserIds)
|
||||
}
|
||||
|
||||
for (const key of ['promptStarters', 'delegatedAuthScopes']) {
|
||||
for (const key of ['promptStarters', 'delegatedAuthScopes', 'attachmentRoots', 'remoteAttachmentRoots']) {
|
||||
if (Object.hasOwn(normalized, key)) normalized[key] = csvToStringArray(normalized[key])
|
||||
}
|
||||
|
||||
for (const key of ['mediaMaxMb', 'historyLimit', 'dmHistoryLimit', 'textChunkLimit', 'rateLimitPerMinute', 'httpPort', 'webhookPort', 'feedbackReflectionCooldownMs']) {
|
||||
for (const key of ['mediaMaxMb', 'historyLimit', 'dmHistoryLimit', 'textChunkLimit', 'probeTimeoutMs', 'rateLimitPerMinute', 'httpPort', 'webhookPort', 'feedbackReflectionCooldownMs']) {
|
||||
if (!Object.hasOwn(normalized, key)) continue
|
||||
const value = String(normalized[key] || '').trim()
|
||||
if (!value) {
|
||||
@@ -2489,7 +2489,7 @@ export function normalizeMessagingPlatformForm(platform, form = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of ['dangerouslyAllowNameMatching', 'dangerouslyAllowPrivateNetwork', 'dangerouslyAllowInheritedWebhookPath', 'allowInsecureSsl', 'allowBots', 'blockStreaming', 'useManagedIdentity', 'typingIndicator', 'welcomeCard', 'groupWelcomeCard', 'feedbackEnabled', 'feedbackReflection', 'delegatedAuthEnabled', 'ssoEnabled']) {
|
||||
for (const key of ['dangerouslyAllowNameMatching', 'dangerouslyAllowPrivateNetwork', 'dangerouslyAllowInheritedWebhookPath', 'allowInsecureSsl', 'allowBots', 'blockStreaming', 'useManagedIdentity', 'typingIndicator', 'welcomeCard', 'groupWelcomeCard', 'feedbackEnabled', 'feedbackReflection', 'delegatedAuthEnabled', 'ssoEnabled', 'configWrites', 'includeAttachments', 'sendReadReceipts', 'coalesceSameSenderDms']) {
|
||||
if (Object.hasOwn(normalized, key)) {
|
||||
const value = String(normalized[key] || '').trim()
|
||||
if (!value) {
|
||||
@@ -2770,7 +2770,7 @@ export function buildOpenClawChannelDiagnosis({
|
||||
.map(group => group.label)
|
||||
const hasAnyCredential = channelRootHasMessagingCredential(form)
|
||||
const anyCredentialOk = anyFields.length ? anyFields.some(([key]) => hasConfiguredMessagingValue(form?.[key])) : false
|
||||
const credentialOk = storageKey === 'zalouser'
|
||||
const credentialOk = ['zalouser', 'imessage'].includes(storageKey)
|
||||
? !!configExists
|
||||
: (requiredFields.length
|
||||
? missing.length === 0
|
||||
@@ -2781,9 +2781,13 @@ export function buildOpenClawChannelDiagnosis({
|
||||
checks.push({
|
||||
id: 'credentials',
|
||||
ok: credentialOk,
|
||||
title: storageKey === 'zalouser' ? '登录/会话配置' : '必要凭证字段',
|
||||
title: storageKey === 'zalouser' ? '登录/会话配置' : (storageKey === 'imessage' ? '桥接运行配置' : '必要凭证字段'),
|
||||
detail: storageKey === 'zalouser'
|
||||
? 'Zalo Personal 通过二维码登录保存本地会话;配置已保存后,请按手动命令完成或刷新登录。'
|
||||
: storageKey === 'imessage'
|
||||
? (configExists
|
||||
? 'iMessage 使用本机或远端桥接运行,不需要 Bot Token;已保存基础运行配置。'
|
||||
: '尚未保存 iMessage 渠道配置,请先填写并保存。')
|
||||
: (credentialOk
|
||||
? (requiredFields.length
|
||||
? `已填写 ${requiredFields.map(([, label]) => label).join(' / ')}。`
|
||||
@@ -3662,6 +3666,22 @@ 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 === 'imessage') {
|
||||
for (const key of ['cliPath', 'dbPath', 'remoteHost', 'service', 'region', 'defaultTo', 'contextVisibility', 'chunkMode', 'reactionNotifications', 'responsePrefix']) {
|
||||
if (form[key]) entry[key] = form[key]
|
||||
}
|
||||
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
|
||||
if (Array.isArray(form.attachmentRoots) && form.attachmentRoots.length) entry.attachmentRoots = form.attachmentRoots
|
||||
if (Array.isArray(form.remoteAttachmentRoots) && form.remoteAttachmentRoots.length) entry.remoteAttachmentRoots = form.remoteAttachmentRoots
|
||||
for (const key of ['configWrites', 'includeAttachments', 'blockStreaming', 'sendReadReceipts', 'coalesceSameSenderDms']) {
|
||||
if (typeof form[key] === 'boolean') entry[key] = form[key]
|
||||
}
|
||||
for (const key of ['historyLimit', 'dmHistoryLimit', 'mediaMaxMb', 'probeTimeoutMs', 'textChunkLimit']) {
|
||||
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]
|
||||
@@ -3766,7 +3786,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', 'msteams'].includes(storageKey)) {
|
||||
if (['zalo', 'zalouser', 'line', 'mattermost', 'synology-chat', 'googlechat', 'msteams', 'imessage'].includes(storageKey)) {
|
||||
ensureMessagingPluginAllowed(cfg, storageKey)
|
||||
}
|
||||
return { entry, accountId: normalizedAccountId, storageKey }
|
||||
|
||||
@@ -334,7 +334,7 @@ fn channel_any_credential_groups(
|
||||
}
|
||||
|
||||
fn channel_diagnosis_credentials_ready(platform: &str, form: &Map<String, Value>) -> bool {
|
||||
if platform_storage_key(platform) == "zalouser" {
|
||||
if matches!(platform_storage_key(platform), "zalouser" | "imessage") {
|
||||
return true;
|
||||
}
|
||||
if platform_storage_key(platform) == "msteams" {
|
||||
@@ -465,7 +465,7 @@ fn build_openclaw_channel_diagnosis(
|
||||
.iter()
|
||||
.any(|(key, _)| has_configured_messaging_value(form.get(*key)))
|
||||
};
|
||||
let credential_ok = if storage_key == "zalouser" {
|
||||
let credential_ok = if matches!(storage_key, "zalouser" | "imessage") {
|
||||
config_exists
|
||||
} else if !required_fields.is_empty() {
|
||||
missing.is_empty()
|
||||
@@ -481,9 +481,21 @@ fn build_openclaw_channel_diagnosis(
|
||||
checks.push(json!({
|
||||
"id": "credentials",
|
||||
"ok": credential_ok,
|
||||
"title": if storage_key == "zalouser" { "登录/会话配置" } else { "必要凭证字段" },
|
||||
"title": if storage_key == "zalouser" {
|
||||
"登录/会话配置"
|
||||
} else if storage_key == "imessage" {
|
||||
"桥接运行配置"
|
||||
} else {
|
||||
"必要凭证字段"
|
||||
},
|
||||
"detail": if storage_key == "zalouser" {
|
||||
"Zalo Personal 通过二维码登录保存本地会话;配置已保存后,请按手动命令完成或刷新登录。".to_string()
|
||||
} else if storage_key == "imessage" {
|
||||
if config_exists {
|
||||
"iMessage 使用本机或远端桥接运行,不需要 Bot Token;已保存基础运行配置。".to_string()
|
||||
} else {
|
||||
"尚未保存 iMessage 渠道配置,请先填写并保存。".to_string()
|
||||
}
|
||||
} else if credential_ok {
|
||||
if !required_fields.is_empty() {
|
||||
format!("已填写 {}。", required_labels)
|
||||
@@ -703,6 +715,16 @@ fn put_number_from_form(entry: &mut Map<String, Value>, key: &str, raw: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
fn put_number_value_if_present(entry: &mut Map<String, Value>, key: &str, value: Option<&Value>) {
|
||||
if let Some(number) = value.and_then(|v| v.as_f64()) {
|
||||
if let Some(json_number) = serde_json::Number::from_f64(number) {
|
||||
entry.insert(key.into(), Value::Number(json_number));
|
||||
}
|
||||
return;
|
||||
}
|
||||
put_number_from_form(entry, key, &value.and_then(|v| v.as_str()).unwrap_or(""));
|
||||
}
|
||||
|
||||
fn normalize_numeric_form_value(map: &mut Map<String, Value>, key: &str) {
|
||||
let Some(value) = map.get(key).cloned() else {
|
||||
return;
|
||||
@@ -801,6 +823,7 @@ fn normalize_messaging_platform_form(
|
||||
| "line"
|
||||
| "mattermost"
|
||||
| "googlechat"
|
||||
| "imessage"
|
||||
);
|
||||
let has_dm_field = normalized.contains_key("dmPolicy") || needs_access_defaults;
|
||||
let has_group_field = normalized.contains_key("groupPolicy") || needs_access_defaults;
|
||||
@@ -865,12 +888,18 @@ fn normalize_messaging_platform_form(
|
||||
normalize_numeric_form_value(&mut normalized, "historyLimit");
|
||||
normalize_numeric_form_value(&mut normalized, "dmHistoryLimit");
|
||||
normalize_numeric_form_value(&mut normalized, "textChunkLimit");
|
||||
normalize_numeric_form_value(&mut normalized, "probeTimeoutMs");
|
||||
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"] {
|
||||
for key in [
|
||||
"promptStarters",
|
||||
"delegatedAuthScopes",
|
||||
"attachmentRoots",
|
||||
"remoteAttachmentRoots",
|
||||
] {
|
||||
if normalized.contains_key(key) {
|
||||
let items = json_array_from_csv_value(normalized.get(key));
|
||||
normalized.insert(key.into(), Value::Array(items));
|
||||
@@ -892,6 +921,10 @@ fn normalize_messaging_platform_form(
|
||||
"feedbackReflection",
|
||||
"delegatedAuthEnabled",
|
||||
"ssoEnabled",
|
||||
"configWrites",
|
||||
"includeAttachments",
|
||||
"sendReadReceipts",
|
||||
"coalesceSameSenderDms",
|
||||
] {
|
||||
if normalized.contains_key(key) {
|
||||
let value = match normalized.get(key) {
|
||||
@@ -1500,6 +1533,44 @@ pub async fn read_platform_config(
|
||||
insert_number_as_string(&mut form, &saved, key);
|
||||
}
|
||||
}
|
||||
"imessage" => {
|
||||
for key in [
|
||||
"cliPath",
|
||||
"dbPath",
|
||||
"remoteHost",
|
||||
"service",
|
||||
"region",
|
||||
"defaultTo",
|
||||
"contextVisibility",
|
||||
"chunkMode",
|
||||
"reactionNotifications",
|
||||
"responsePrefix",
|
||||
] {
|
||||
insert_string_if_present(&mut form, &saved, key);
|
||||
}
|
||||
insert_access_policy_form_values(&mut form, &saved, false, false);
|
||||
insert_array_as_csv(&mut form, &saved, "groupAllowFrom");
|
||||
insert_array_as_csv(&mut form, &saved, "attachmentRoots");
|
||||
insert_array_as_csv(&mut form, &saved, "remoteAttachmentRoots");
|
||||
for key in [
|
||||
"configWrites",
|
||||
"includeAttachments",
|
||||
"blockStreaming",
|
||||
"sendReadReceipts",
|
||||
"coalesceSameSenderDms",
|
||||
] {
|
||||
insert_bool_as_string(&mut form, &saved, key);
|
||||
}
|
||||
for key in [
|
||||
"historyLimit",
|
||||
"dmHistoryLimit",
|
||||
"mediaMaxMb",
|
||||
"probeTimeoutMs",
|
||||
"textChunkLimit",
|
||||
] {
|
||||
insert_number_as_string(&mut form, &saved, key);
|
||||
}
|
||||
}
|
||||
"matrix" => {
|
||||
insert_string_if_present(&mut form, &saved, "homeserver");
|
||||
insert_secret_aware_form_value(&mut form, &saved, "accessToken");
|
||||
@@ -2248,6 +2319,67 @@ pub async fn save_messaging_platform(
|
||||
entry,
|
||||
)?;
|
||||
}
|
||||
"imessage" => {
|
||||
let mut entry = Map::new();
|
||||
entry.insert("enabled".into(), Value::Bool(true));
|
||||
for key in [
|
||||
"cliPath",
|
||||
"dbPath",
|
||||
"remoteHost",
|
||||
"service",
|
||||
"region",
|
||||
"defaultTo",
|
||||
"contextVisibility",
|
||||
"chunkMode",
|
||||
"reactionNotifications",
|
||||
"responsePrefix",
|
||||
] {
|
||||
put_string(&mut entry, key, form_string(form_obj, key));
|
||||
}
|
||||
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"));
|
||||
put_array_from_form_value(&mut entry, "groupAllowFrom", form_obj.get("groupAllowFrom"));
|
||||
put_array_from_form_value(
|
||||
&mut entry,
|
||||
"attachmentRoots",
|
||||
form_obj.get("attachmentRoots"),
|
||||
);
|
||||
put_array_from_form_value(
|
||||
&mut entry,
|
||||
"remoteAttachmentRoots",
|
||||
form_obj.get("remoteAttachmentRoots"),
|
||||
);
|
||||
for key in [
|
||||
"configWrites",
|
||||
"includeAttachments",
|
||||
"blockStreaming",
|
||||
"sendReadReceipts",
|
||||
"coalesceSameSenderDms",
|
||||
] {
|
||||
put_bool_value_if_present(&mut entry, key, form_obj.get(key));
|
||||
}
|
||||
for key in [
|
||||
"historyLimit",
|
||||
"dmHistoryLimit",
|
||||
"mediaMaxMb",
|
||||
"probeTimeoutMs",
|
||||
"textChunkLimit",
|
||||
] {
|
||||
put_number_value_if_present(&mut entry, key, form_obj.get(key));
|
||||
}
|
||||
merge_channel_entry_for_account(
|
||||
channels_map,
|
||||
&storage_key,
|
||||
account_id.as_deref(),
|
||||
entry,
|
||||
)?;
|
||||
ensure_plugin_allowed(&mut cfg, "imessage")?;
|
||||
}
|
||||
"matrix" => {
|
||||
let homeserver = form_string(form_obj, "homeserver");
|
||||
let access_token = form_string(form_obj, "accessToken");
|
||||
@@ -2825,6 +2957,10 @@ pub async fn verify_bot_token(platform: String, form: Value) -> Result<Value, St
|
||||
"matrix" => verify_matrix(&client, form_obj).await,
|
||||
"signal" => verify_signal(&client, form_obj).await,
|
||||
"msteams" => verify_msteams(&client, form_obj).await,
|
||||
"imessage" => Ok(json!({
|
||||
"valid": true,
|
||||
"warnings": ["iMessage 使用本机或远端桥接运行,无需在线校验 Bot Token;请通过 Gateway 日志确认桥接进程状态"]
|
||||
})),
|
||||
"whatsapp" => Ok(json!({
|
||||
"valid": true,
|
||||
"warnings": ["WhatsApp 使用扫码登录,无需在线校验凭证;请通过「启动扫码登录」完成配对"]
|
||||
@@ -5768,6 +5904,62 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_imessage_form_preserves_bridge_runtime_fields() {
|
||||
let form = json!({
|
||||
"dmPolicy": "allowlist",
|
||||
"allowFrom": "+15551234567, +15557654321",
|
||||
"groupPolicy": "allowlist",
|
||||
"groupAllowFrom": "chat-guid-1, chat-guid-2",
|
||||
"probeTimeoutMs": "5000",
|
||||
"attachmentRoots": "/Users/me/Downloads, /tmp/imessage",
|
||||
"includeAttachments": "true",
|
||||
"sendReadReceipts": "false"
|
||||
});
|
||||
let normalized =
|
||||
normalize_messaging_platform_form("imessage", form.as_object().expect("object"));
|
||||
|
||||
assert_eq!(
|
||||
normalized.get("dmPolicy").and_then(|v| v.as_str()),
|
||||
Some("allowlist")
|
||||
);
|
||||
assert_eq!(
|
||||
normalized.get("probeTimeoutMs").and_then(|v| v.as_f64()),
|
||||
Some(5000.0)
|
||||
);
|
||||
assert_eq!(
|
||||
normalized
|
||||
.get("includeAttachments")
|
||||
.and_then(|v| v.as_bool()),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
normalized.get("sendReadReceipts").and_then(|v| v.as_bool()),
|
||||
Some(false)
|
||||
);
|
||||
assert_eq!(
|
||||
normalized
|
||||
.get("attachmentRoots")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|items| items.len()),
|
||||
Some(2)
|
||||
);
|
||||
assert!(channel_diagnosis_credentials_ready("imessage", &normalized));
|
||||
let diagnosis =
|
||||
build_openclaw_channel_diagnosis("imessage", None, true, true, &normalized, None, None);
|
||||
assert_eq!(
|
||||
diagnosis
|
||||
.get("checks")
|
||||
.and_then(|v| v.as_array())
|
||||
.and_then(|items| items
|
||||
.iter()
|
||||
.find(|item| item.get("id").and_then(|v| v.as_str()) == Some("credentials")))
|
||||
.and_then(|item| item.get("title"))
|
||||
.and_then(|v| v.as_str()),
|
||||
Some("桥接运行配置")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_form_readback_preserves_mention_policy_choice() {
|
||||
let saved = json!({
|
||||
|
||||
@@ -234,6 +234,21 @@ export default {
|
||||
signalAllowFromPh: _('可选,逗号分隔', 'Optional, comma-separated', '可選,逗號分隔'),
|
||||
signalGroupAllowFromPh: _('可选,逗号分隔群组 ID', 'Optional, comma-separated group IDs', '可選,逗號分隔群組 ID'),
|
||||
signalBlockStreaming: _('阻止流式分块', 'Block streaming chunks', '阻止串流分塊'),
|
||||
imessageDesc: _('接入 iMessage / SMS 桥接,支持本机或远端 Mac 运行', 'Connect iMessage / SMS bridge from a local or remote Mac', '接入 iMessage / SMS 橋接,支援本機或遠端 Mac 執行'),
|
||||
imessageGuide1: _('在可访问 Messages 数据库的 macOS 设备上安装 iMessage 桥接插件', 'Install the iMessage bridge plugin on a macOS device that can access the Messages database', '在可存取 Messages 資料庫的 macOS 裝置上安裝 iMessage 橋接外掛'),
|
||||
imessageGuide2: _('确认桥接 CLI 可读取 chat.db,并能按需发送 iMessage 或 SMS', 'Confirm the bridge CLI can read chat.db and send iMessage or SMS when needed', '確認橋接 CLI 可讀取 chat.db,並能按需傳送 iMessage 或 SMS'),
|
||||
imessageGuide3: _('按需填写远端主机、白名单、附件目录和历史窗口', 'Fill remote host, allowlists, attachment roots, and history windows as needed', '按需填寫遠端主機、白名單、附件目錄和歷史視窗'),
|
||||
imessageGuide4: _('保存后重启或重载 Gateway,再通过渠道诊断查看桥接状态', 'Save, restart or reload Gateway, then check bridge status in channel diagnostics', '儲存後重啟或重載 Gateway,再透過頻道診斷查看橋接狀態'),
|
||||
imessageGuideFooter: _('<div style="margin-top:8px;font-size:var(--font-size-xs);color:var(--text-tertiary)">iMessage 不需要 Bot Token;运行状态取决于本机或远端 Mac 的桥接进程权限</div>', '<div style="margin-top:8px;font-size:var(--font-size-xs);color:var(--text-tertiary)">iMessage does not need a Bot Token; runtime health depends on bridge process permissions on the local or remote Mac</div>', '<div style="margin-top:8px;font-size:var(--font-size-xs);color:var(--text-tertiary)">iMessage 不需要 Bot Token;執行狀態取決於本機或遠端 Mac 的橋接程序權限</div>'),
|
||||
imessageCliPathPh: _('可选,默认从 PATH 查找桥接 CLI', 'Optional, defaults to PATH lookup for the bridge CLI', '可選,預設從 PATH 尋找橋接 CLI'),
|
||||
imessageAllowFromPh: _('可选,逗号分隔手机号、邮箱或联系人 ID', 'Optional, comma-separated phone numbers, emails, or contact IDs', '可選,逗號分隔手機號、Email 或聯絡人 ID'),
|
||||
imessageGroupAllowFromPh: _('可选,逗号分隔群聊 GUID', 'Optional, comma-separated group chat GUIDs', '可選,逗號分隔群聊 GUID'),
|
||||
imessageIncludeAttachments: _('包含附件', 'Include attachments', '包含附件'),
|
||||
imessageAttachmentRootsPh: _('可选,逗号分隔本机附件目录', 'Optional, comma-separated local attachment directories', '可選,逗號分隔本機附件目錄'),
|
||||
imessageRemoteAttachmentRootsPh: _('可选,逗号分隔远端附件目录', 'Optional, comma-separated remote attachment directories', '可選,逗號分隔遠端附件目錄'),
|
||||
imessageReadReceipts: _('发送已读回执', 'Send read receipts', '傳送已讀回執'),
|
||||
imessageReactionOwn: _('仅自己相关', 'Own only', '僅自己相關'),
|
||||
imessageCoalesceDms: _('合并同发送者私信', 'Coalesce same-sender DMs', '合併同傳送者私訊'),
|
||||
matrixDesc: _('接入 Matrix 协议(Element 等客户端)', 'Connect via Matrix protocol (Element and other clients)', '接入 Matrix 协議(Element 等用戶端)', 'Matrix プロトコルに接続'),
|
||||
matrixGuide1: _('在 Matrix 服务器上注册 Bot 账号', 'Register a Bot account on a Matrix server', '在 Matrix 伺服器上註冊 Bot 账號'),
|
||||
matrixGuide2: _('获取 <strong>Access Token</strong>(或使用用户名密码)', 'Get an <strong>Access Token</strong> (or use username & password)', '取得 <strong>Access Token</strong>(或使用使用者名密碼)'),
|
||||
@@ -244,6 +259,7 @@ export default {
|
||||
matrixAllowFromPh: _('可选,逗号分隔用户 ID', 'Optional, comma-separated user IDs', '可選,逗號分隔使用者 ID'),
|
||||
matrixAuthRequired: _('Matrix 需要填写 Access Token,或填写 User ID + Password', 'Matrix requires an Access Token, or User ID + Password'),
|
||||
groupAllGroups: _('所有群组', 'All groups', '所有群組'),
|
||||
groupAllChats: _('所有聊天', 'All chats', '所有聊天'),
|
||||
groupAllRooms: _('所有房间', 'All rooms', '所有房間'),
|
||||
groupAllTeams: _('所有团队', 'All teams', '所有團队'),
|
||||
groupMentionBot: _('仅 @机器人时', 'Only when @bot', '僅 @機器人時'),
|
||||
|
||||
@@ -492,6 +492,56 @@ const PLATFORM_REGISTRY = {
|
||||
],
|
||||
configKey: 'signal',
|
||||
},
|
||||
imessage: {
|
||||
label: 'iMessage',
|
||||
iconName: 'message-circle',
|
||||
desc: t('channels.imessageDesc'),
|
||||
guide: [
|
||||
t('channels.imessageGuide1'),
|
||||
t('channels.imessageGuide2'),
|
||||
t('channels.imessageGuide3'),
|
||||
t('channels.imessageGuide4'),
|
||||
],
|
||||
guideFooter: t('channels.imessageGuideFooter'),
|
||||
fields: [
|
||||
{ key: 'cliPath', label: 'Bridge CLI Path', placeholder: t('channels.imessageCliPathPh'), required: false },
|
||||
{ key: 'dbPath', label: 'Messages DB Path', placeholder: '~/Library/Messages/chat.db', required: false },
|
||||
{ key: 'remoteHost', label: 'Remote Host', placeholder: t('channels.optionalEg', { example: 'mac-mini.local' }), required: false },
|
||||
{ key: 'service', label: 'Service', type: 'select', options: [
|
||||
{ value: '', label: t('channels.policyDefault') },
|
||||
{ value: 'imessage', label: 'iMessage' },
|
||||
{ value: 'sms', label: 'SMS' },
|
||||
{ value: 'auto', label: 'Auto' },
|
||||
], required: false },
|
||||
{ key: 'region', label: 'Region', placeholder: t('channels.optionalEg', { example: 'US' }), 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.groupAllChats')), required: false },
|
||||
{ key: 'allowFrom', label: 'Allow From', placeholder: t('channels.imessageAllowFromPh'), required: false, hint: t('channels.allowFromHint') },
|
||||
{ key: 'defaultTo', label: 'Default To', placeholder: t('channels.optionalEg', { example: '+15550001111' }), required: false },
|
||||
{ key: 'groupAllowFrom', label: 'Group Allow From', placeholder: t('channels.imessageGroupAllowFromPh'), 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: 'mediaMaxMb', label: 'Media Max MB', placeholder: '25', required: false },
|
||||
{ key: 'probeTimeoutMs', label: 'Probe Timeout MS', placeholder: '5000', required: false },
|
||||
{ key: 'textChunkLimit', label: 'Text Chunk Limit', placeholder: '1800', required: false },
|
||||
{ key: 'includeAttachments', label: t('channels.imessageIncludeAttachments'), type: 'select', options: BOOLEAN_OPTIONS, required: false },
|
||||
{ key: 'attachmentRoots', label: 'Attachment Roots', placeholder: t('channels.imessageAttachmentRootsPh'), required: false },
|
||||
{ key: 'remoteAttachmentRoots', label: 'Remote Attachment Roots', placeholder: t('channels.imessageRemoteAttachmentRootsPh'), required: false },
|
||||
{ key: 'blockStreaming', label: t('channels.signalBlockStreaming'), type: 'select', options: BOOLEAN_OPTIONS, required: false },
|
||||
{ key: 'sendReadReceipts', label: t('channels.imessageReadReceipts'), type: 'select', options: BOOLEAN_OPTIONS, required: false },
|
||||
{ key: 'reactionNotifications', label: 'Reaction Notifications', type: 'select', options: [
|
||||
{ value: '', label: t('channels.policyDefault') },
|
||||
{ value: 'off', label: t('channels.disable') },
|
||||
{ value: 'all', label: t('channels.enable') },
|
||||
{ value: 'own', label: t('channels.imessageReactionOwn') },
|
||||
], required: false },
|
||||
{ key: 'coalesceSameSenderDms', label: t('channels.imessageCoalesceDms'), type: 'select', options: BOOLEAN_OPTIONS, required: false },
|
||||
{ key: 'responsePrefix', label: 'Response Prefix', placeholder: t('channels.optionalEg', { example: '[iMessage]' }), required: false },
|
||||
],
|
||||
configKey: 'imessage',
|
||||
pluginRequired: '@openclaw/imessage@latest',
|
||||
pluginId: 'imessage',
|
||||
},
|
||||
matrix: {
|
||||
label: 'Matrix',
|
||||
iconName: 'globe',
|
||||
|
||||
@@ -920,6 +920,100 @@ test('Microsoft Teams 诊断会按认证模式检查动态必填凭证', () => {
|
||||
assert.equal(managedIdentity.checks.find(item => item.id === 'credentials')?.ok, true)
|
||||
})
|
||||
|
||||
test('iMessage 渠道保存会写入桥接运行字段并启用插件', () => {
|
||||
const cfg = { channels: {} }
|
||||
|
||||
mergeOpenClawMessagingPlatformConfig(cfg, {
|
||||
platform: 'imessage',
|
||||
form: {
|
||||
cliPath: '/usr/local/bin/imsg',
|
||||
dbPath: '~/Library/Messages/chat.db',
|
||||
remoteHost: 'mac-mini.local',
|
||||
service: 'auto',
|
||||
region: 'US',
|
||||
dmPolicy: 'allowlist',
|
||||
allowFrom: '+15551234567, +15557654321',
|
||||
defaultTo: '+15550001111',
|
||||
groupPolicy: 'allowlist',
|
||||
groupAllowFrom: 'chat-guid-1, chat-guid-2',
|
||||
historyLimit: '80',
|
||||
dmHistoryLimit: '20',
|
||||
mediaMaxMb: '25',
|
||||
probeTimeoutMs: '5000',
|
||||
textChunkLimit: '1800',
|
||||
includeAttachments: 'true',
|
||||
attachmentRoots: '/Users/me/Downloads, /tmp/imessage',
|
||||
remoteAttachmentRoots: '/mnt/messages',
|
||||
sendReadReceipts: 'false',
|
||||
coalesceSameSenderDms: 'true',
|
||||
reactionNotifications: 'own',
|
||||
responsePrefix: '[iMessage]',
|
||||
},
|
||||
})
|
||||
|
||||
const entry = cfg.channels.imessage
|
||||
assert.equal(entry.cliPath, '/usr/local/bin/imsg')
|
||||
assert.equal(entry.dbPath, '~/Library/Messages/chat.db')
|
||||
assert.equal(entry.remoteHost, 'mac-mini.local')
|
||||
assert.equal(entry.service, 'auto')
|
||||
assert.equal(entry.region, 'US')
|
||||
assert.equal(entry.dmPolicy, 'allowlist')
|
||||
assert.deepEqual(entry.allowFrom, ['+15551234567', '+15557654321'])
|
||||
assert.equal(entry.defaultTo, '+15550001111')
|
||||
assert.equal(entry.groupPolicy, 'allowlist')
|
||||
assert.deepEqual(entry.groupAllowFrom, ['chat-guid-1', 'chat-guid-2'])
|
||||
assert.equal(entry.historyLimit, 80)
|
||||
assert.equal(entry.dmHistoryLimit, 20)
|
||||
assert.equal(entry.mediaMaxMb, 25)
|
||||
assert.equal(entry.probeTimeoutMs, 5000)
|
||||
assert.equal(entry.textChunkLimit, 1800)
|
||||
assert.equal(entry.includeAttachments, true)
|
||||
assert.deepEqual(entry.attachmentRoots, ['/Users/me/Downloads', '/tmp/imessage'])
|
||||
assert.deepEqual(entry.remoteAttachmentRoots, ['/mnt/messages'])
|
||||
assert.equal(entry.sendReadReceipts, false)
|
||||
assert.equal(entry.coalesceSameSenderDms, true)
|
||||
assert.equal(entry.reactionNotifications, 'own')
|
||||
assert.equal(entry.responsePrefix, '[iMessage]')
|
||||
assert.equal(cfg.plugins.entries.imessage.enabled, true)
|
||||
})
|
||||
|
||||
test('iMessage 读取会回显桥接字段且诊断不要求 Bot Token', () => {
|
||||
const values = buildMessagingPlatformFormValues('imessage', {
|
||||
cliPath: '/usr/local/bin/imsg',
|
||||
dbPath: '~/Library/Messages/chat.db',
|
||||
remoteHost: 'mac-mini.local',
|
||||
service: 'sms',
|
||||
dmPolicy: 'open',
|
||||
allowFrom: ['*'],
|
||||
groupPolicy: 'allowlist',
|
||||
groupAllowFrom: ['chat-guid-1'],
|
||||
includeAttachments: true,
|
||||
attachmentRoots: ['/Users/me/Downloads'],
|
||||
sendReadReceipts: false,
|
||||
historyLimit: 50,
|
||||
responsePrefix: '[iMessage]',
|
||||
})
|
||||
const diagnosis = buildOpenClawChannelDiagnosis({
|
||||
platform: 'imessage',
|
||||
configExists: true,
|
||||
channelEnabled: true,
|
||||
form: values,
|
||||
})
|
||||
|
||||
assert.equal(values.cliPath, '/usr/local/bin/imsg')
|
||||
assert.equal(values.service, 'sms')
|
||||
assert.equal(values.dmPolicy, 'open')
|
||||
assert.equal(values.allowFrom, '*')
|
||||
assert.equal(values.groupAllowFrom, 'chat-guid-1')
|
||||
assert.equal(values.includeAttachments, 'true')
|
||||
assert.equal(values.attachmentRoots, '/Users/me/Downloads')
|
||||
assert.equal(values.sendReadReceipts, 'false')
|
||||
assert.equal(values.historyLimit, '50')
|
||||
assert.equal(values.responsePrefix, '[iMessage]')
|
||||
assert.equal(diagnosis.checks.find(item => item.id === 'credentials')?.ok, true)
|
||||
assert.match(diagnosis.checks.find(item => item.id === 'credentials')?.title || '', /桥接/)
|
||||
})
|
||||
|
||||
test('Discord 渠道保存会保留运行时需要的 applicationId', () => {
|
||||
const cfg = { channels: {} }
|
||||
|
||||
|
||||
@@ -17,3 +17,25 @@ test('Discord 渠道 UI 会暴露服务器频道 allowlist 配置字段', () =>
|
||||
assert.match(discordBlock, /key:\s*'guildId'/)
|
||||
assert.match(discordBlock, /key:\s*'channelId'/)
|
||||
})
|
||||
|
||||
test('iMessage 渠道 UI 会暴露桥接运行配置字段', () => {
|
||||
const imessageBlock = getRegistryBlock('imessage')
|
||||
|
||||
for (const field of [
|
||||
'cliPath',
|
||||
'dbPath',
|
||||
'remoteHost',
|
||||
'service',
|
||||
'allowFrom',
|
||||
'groupAllowFrom',
|
||||
'probeTimeoutMs',
|
||||
'attachmentRoots',
|
||||
'remoteAttachmentRoots',
|
||||
'sendReadReceipts',
|
||||
'coalesceSameSenderDms',
|
||||
]) {
|
||||
assert.match(imessageBlock, new RegExp(`key:\\s*'${field}'`))
|
||||
}
|
||||
assert.match(imessageBlock, /pluginRequired:\s*'@openclaw\/imessage@latest'/)
|
||||
assert.match(imessageBlock, /pluginId:\s*'imessage'/)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user