From 49be118c5f60b91d396056e3ba65f4a64dd01796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E5=A4=A9?= Date: Sat, 23 May 2026 05:56:59 +0800 Subject: [PATCH] feat(channels): improve Signal config compatibility --- scripts/dev-api.js | 22 +++++++- src-tauri/src/commands/messaging.rs | 37 ++++++++++++- src/locales/modules/channels.js | 2 + src/pages/channels.js | 9 +++- src/style/layout.css | 6 ++- tests/channel-config-normalization.test.js | 63 ++++++++++++++++++++++ 6 files changed, 132 insertions(+), 7 deletions(-) diff --git a/scripts/dev-api.js b/scripts/dev-api.js index 8f8d4ba..e6921e9 100644 --- a/scripts/dev-api.js +++ b/scripts/dev-api.js @@ -2472,7 +2472,7 @@ export function normalizeMessagingPlatformForm(platform, form = {}) { normalized.allowedUserIds = csvToStringArray(normalized.allowedUserIds) } - for (const key of ['mediaMaxMb', 'historyLimit', 'dmHistoryLimit', 'textChunkLimit', 'rateLimitPerMinute']) { + for (const key of ['mediaMaxMb', 'historyLimit', 'dmHistoryLimit', 'textChunkLimit', 'rateLimitPerMinute', 'httpPort']) { if (!Object.hasOwn(normalized, key)) continue const value = String(normalized[key] || '').trim() if (!value) { @@ -2883,10 +2883,15 @@ export function buildMessagingPlatformFormValues(platform, saved = {}, options = } if (storageKey === 'signal') { - for (const key of ['account', 'cliPath', 'httpUrl', 'httpHost', 'httpPort']) { + for (const key of ['account', 'cliPath', 'httpUrl', 'httpHost', 'httpPort', 'responsePrefix']) { putSecretAwareFormValue(form, saved, key) } putAccessPolicyFormValues(form, saved) + putCsvFormValue(form, saved, 'groupAllowFrom') + putBoolFormValue(form, saved, 'blockStreaming') + for (const key of ['historyLimit', 'dmHistoryLimit', 'textChunkLimit', 'mediaMaxMb']) { + if (typeof saved[key] === 'number') form[key] = String(saved[key]) + } return form } @@ -3494,6 +3499,19 @@ function buildOpenClawMessagingPlatformEntry(platform, form, currentSaved = {}) 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 (typeof form.mediaMaxMb === 'number') entry.mediaMaxMb = form.mediaMaxMb + } else if (storageKey === 'signal') { + for (const key of ['account', 'cliPath', 'httpUrl', 'httpHost', 'responsePrefix']) { + if (form[key]) entry[key] = form[key] + } + if (typeof form.httpPort === 'number') entry.httpPort = form.httpPort + 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 (typeof form.blockStreaming === 'boolean') entry.blockStreaming = form.blockStreaming + for (const key of ['historyLimit', 'dmHistoryLimit', 'textChunkLimit', 'mediaMaxMb']) { + if (typeof form[key] === 'number') entry[key] = form[key] + } } else if (storageKey === 'zalouser') { for (const key of ['profile', 'messagePrefix', 'responsePrefix']) { if (form[key]) entry[key] = form[key] diff --git a/src-tauri/src/commands/messaging.rs b/src-tauri/src/commands/messaging.rs index ae17b32..fe78e7d 100644 --- a/src-tauri/src/commands/messaging.rs +++ b/src-tauri/src/commands/messaging.rs @@ -552,6 +552,12 @@ fn insert_array_as_csv(form: &mut Map, source: &Value, key: &str) } } +fn insert_number_as_string(form: &mut Map, source: &Value, key: &str) { + if let Some(v) = source.get(key).and_then(|v| v.as_f64()) { + form.insert(key.into(), Value::String(v.to_string())); + } +} + fn insert_access_policy_form_values( form: &mut Map, source: &Value, @@ -809,6 +815,7 @@ fn normalize_messaging_platform_form( normalize_numeric_form_value(&mut normalized, "dmHistoryLimit"); normalize_numeric_form_value(&mut normalized, "textChunkLimit"); normalize_numeric_form_value(&mut normalized, "rateLimitPerMinute"); + normalize_numeric_form_value(&mut normalized, "httpPort"); for key in [ "dangerouslyAllowNameMatching", @@ -1411,8 +1418,19 @@ pub async fn read_platform_config( insert_string_if_present(&mut form, &saved, "cliPath"); 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_number_as_string(&mut form, &saved, "httpPort"); + insert_string_if_present(&mut form, &saved, "responsePrefix"); insert_access_policy_form_values(&mut form, &saved, false, false); + insert_array_as_csv(&mut form, &saved, "groupAllowFrom"); + insert_bool_as_string(&mut form, &saved, "blockStreaming"); + for key in [ + "historyLimit", + "dmHistoryLimit", + "textChunkLimit", + "mediaMaxMb", + ] { + insert_number_as_string(&mut form, &saved, key); + } } "matrix" => { insert_string_if_present(&mut form, &saved, "homeserver"); @@ -2067,7 +2085,12 @@ pub async fn save_messaging_platform( put_string(&mut entry, "cliPath", form_string(form_obj, "cliPath")); put_string(&mut entry, "httpUrl", form_string(form_obj, "httpUrl")); put_string(&mut entry, "httpHost", form_string(form_obj, "httpHost")); - put_string(&mut entry, "httpPort", form_string(form_obj, "httpPort")); + put_number_from_form(&mut entry, "httpPort", &form_string(form_obj, "httpPort")); + put_string( + &mut entry, + "responsePrefix", + form_string(form_obj, "responsePrefix"), + ); put_string(&mut entry, "dmPolicy", form_string(form_obj, "dmPolicy")); put_string( &mut entry, @@ -2075,6 +2098,16 @@ pub async fn save_messaging_platform( 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_bool_value_if_present(&mut entry, "blockStreaming", form_obj.get("blockStreaming")); + for key in [ + "historyLimit", + "dmHistoryLimit", + "textChunkLimit", + "mediaMaxMb", + ] { + put_number_from_form(&mut entry, key, &form_string(form_obj, key)); + } preserve_messaging_credential_refs(&mut entry, form_obj, ¤t_saved); merge_channel_entry_for_account( channels_map, diff --git a/src/locales/modules/channels.js b/src/locales/modules/channels.js index 87bf4fb..e13548e 100644 --- a/src/locales/modules/channels.js +++ b/src/locales/modules/channels.js @@ -228,6 +228,8 @@ export default { signalCliPathLabel: _('signal-cli 路径', 'signal-cli Path', 'signal-cli 路徑'), signalCliPathPh: _('可选,默认从 PATH 查找', 'Optional, defaults to PATH lookup', '可選,預設從 PATH 尋找'), signalAllowFromPh: _('可选,逗号分隔', 'Optional, comma-separated', '可選,逗號分隔'), + signalGroupAllowFromPh: _('可选,逗号分隔群组 ID', 'Optional, comma-separated group IDs', '可選,逗號分隔群組 ID'), + signalBlockStreaming: _('阻止流式分块', 'Block streaming chunks', '阻止串流分塊'), 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: _('获取 Access Token(或使用用户名密码)', 'Get an Access Token (or use username & password)', '取得 Access Token(或使用使用者名密碼)'), diff --git a/src/pages/channels.js b/src/pages/channels.js index 4dc8fe9..6c454c4 100644 --- a/src/pages/channels.js +++ b/src/pages/channels.js @@ -461,6 +461,13 @@ const PLATFORM_REGISTRY = { { 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 }, + { key: 'groupAllowFrom', label: 'Group Allow From', placeholder: t('channels.signalGroupAllowFromPh'), 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: '25', required: false }, + { key: 'blockStreaming', label: t('channels.signalBlockStreaming'), type: 'select', options: BOOLEAN_OPTIONS, required: false }, + { key: 'responsePrefix', label: 'Response Prefix', placeholder: t('channels.optionalEg', { example: '[Signal]' }), required: false }, ], configKey: 'signal', }, @@ -650,7 +657,7 @@ function applyRouteIntent(page, state) { // ── 已配置平台渲染 ── // ── 多账号支持的平台:与 OpenClaw 的 accounts/defaultAccount 配置模型保持一致 ── -const MULTI_INSTANCE_PLATFORMS = ['telegram', 'discord', 'slack', 'feishu', 'dingtalk', 'dingtalk-connector', 'qqbot', 'zalo', 'zalouser', 'line', 'mattermost', 'synology-chat', 'googlechat'] +const MULTI_INSTANCE_PLATFORMS = ['telegram', 'discord', 'slack', 'feishu', 'dingtalk', 'dingtalk-connector', 'qqbot', 'zalo', 'zalouser', 'line', 'mattermost', 'synology-chat', 'googlechat', 'signal'] function supportsMessagingMultiAccount(pid) { return MULTI_INSTANCE_PLATFORMS.includes(pid) diff --git a/src/style/layout.css b/src/style/layout.css index 8234f68..0eb33c7 100644 --- a/src/style/layout.css +++ b/src/style/layout.css @@ -832,14 +832,16 @@ left: 0; top: 0; z-index: 900; - transform: translateX(-100%); + transform: translate3d(-100%, 0, 0) !important; transition: transform 250ms cubic-bezier(.22,1,.36,1); box-shadow: none; width: 260px; + pointer-events: none; } #sidebar.sidebar-open { - transform: translateX(0); + transform: translate3d(0, 0, 0) !important; box-shadow: 4px 0 24px rgba(0,0,0,.15); + pointer-events: auto; } #main-col { width: 100vw; diff --git a/tests/channel-config-normalization.test.js b/tests/channel-config-normalization.test.js index 61c270b..5420eac 100644 --- a/tests/channel-config-normalization.test.js +++ b/tests/channel-config-normalization.test.js @@ -49,6 +49,69 @@ test('渠道保存不会向不支持顶层 requireMention 的平台写入非法 assert.equal(Object.hasOwn(form, 'requireMention'), false) }) +test('Signal 渠道保存会保留多账号和上游运行字段', () => { + const cfg = { channels: {} } + + mergeOpenClawMessagingPlatformConfig(cfg, { + platform: 'signal', + accountId: 'phone-a', + form: { + account: '+15551234567', + cliPath: 'signal-cli', + httpUrl: 'http://127.0.0.1:8080', + dmPolicy: 'allowlist', + allowFrom: '+15550000001', + groupPolicy: 'allowlist', + groupAllowFrom: 'group-1, group-2', + mediaMaxMb: '25', + historyLimit: '80', + dmHistoryLimit: '20', + textChunkLimit: '1800', + blockStreaming: 'true', + responsePrefix: '[Signal]', + }, + }) + + const root = cfg.channels.signal + const account = root.accounts['phone-a'] + assert.equal(root.defaultAccount, 'phone-a') + assert.equal(account.account, '+15551234567') + assert.equal(account.cliPath, 'signal-cli') + assert.equal(account.httpUrl, 'http://127.0.0.1:8080') + assert.equal(account.dmPolicy, 'allowlist') + assert.deepEqual(account.allowFrom, ['+15550000001']) + assert.equal(account.groupPolicy, 'allowlist') + assert.deepEqual(account.groupAllowFrom, ['group-1', 'group-2']) + assert.equal(account.mediaMaxMb, 25) + assert.equal(account.historyLimit, 80) + assert.equal(account.dmHistoryLimit, 20) + assert.equal(account.textChunkLimit, 1800) + assert.equal(account.blockStreaming, true) + assert.equal(account.responsePrefix, '[Signal]') +}) + +test('Signal 渠道读取会回显群组和运行字段', () => { + const values = buildMessagingPlatformFormValues('signal', { + account: '+15551234567', + groupAllowFrom: ['group-1', 'group-2'], + mediaMaxMb: 25, + historyLimit: 80, + dmHistoryLimit: 20, + textChunkLimit: 1800, + blockStreaming: true, + responsePrefix: '[Signal]', + }) + + assert.equal(values.account, '+15551234567') + assert.equal(values.groupAllowFrom, 'group-1, group-2') + assert.equal(values.mediaMaxMb, '25') + assert.equal(values.historyLimit, '80') + assert.equal(values.dmHistoryLimit, '20') + assert.equal(values.textChunkLimit, '1800') + assert.equal(values.blockStreaming, 'true') + assert.equal(values.responsePrefix, '[Signal]') +}) + test('渠道保存会为飞书补齐新版内核要求的默认字段', () => { const form = normalizeMessagingPlatformForm('feishu', { appId: 'cli_a',