mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-29 20:30:00 +08:00
feat(channels): add ClickClack config compatibility
This commit is contained in:
@@ -2472,11 +2472,11 @@ export function normalizeMessagingPlatformForm(platform, form = {}) {
|
||||
normalized.allowedUserIds = csvToStringArray(normalized.allowedUserIds)
|
||||
}
|
||||
|
||||
for (const key of ['promptStarters', 'delegatedAuthScopes', 'attachmentRoots', 'remoteAttachmentRoots']) {
|
||||
for (const key of ['promptStarters', 'delegatedAuthScopes', 'attachmentRoots', 'remoteAttachmentRoots', 'toolsAllow']) {
|
||||
if (Object.hasOwn(normalized, key)) normalized[key] = csvToStringArray(normalized[key])
|
||||
}
|
||||
|
||||
for (const key of ['mediaMaxMb', 'historyLimit', 'dmHistoryLimit', 'textChunkLimit', 'probeTimeoutMs', 'debounceMs', 'rateLimitPerMinute', 'httpPort', 'webhookPort', 'feedbackReflectionCooldownMs']) {
|
||||
for (const key of ['mediaMaxMb', 'historyLimit', 'dmHistoryLimit', 'textChunkLimit', 'probeTimeoutMs', 'debounceMs', 'rateLimitPerMinute', 'httpPort', 'webhookPort', 'feedbackReflectionCooldownMs', 'timeoutSeconds', 'reconnectMs']) {
|
||||
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', 'configWrites', 'includeAttachments', 'sendReadReceipts', 'coalesceSameSenderDms', 'selfChatMode', 'ackDirect']) {
|
||||
for (const key of ['dangerouslyAllowNameMatching', 'dangerouslyAllowPrivateNetwork', 'dangerouslyAllowInheritedWebhookPath', 'allowInsecureSsl', 'enabled', 'allowBots', 'blockStreaming', 'useManagedIdentity', 'typingIndicator', 'welcomeCard', 'groupWelcomeCard', 'feedbackEnabled', 'feedbackReflection', 'delegatedAuthEnabled', 'ssoEnabled', 'configWrites', 'includeAttachments', 'sendReadReceipts', 'coalesceSameSenderDms', 'selfChatMode', 'ackDirect', 'senderIsOwner']) {
|
||||
if (Object.hasOwn(normalized, key)) {
|
||||
const value = String(normalized[key] || '').trim()
|
||||
if (!value) {
|
||||
@@ -2682,6 +2682,7 @@ const CHANNEL_DIAG_REQUIRED_FIELDS = {
|
||||
'dingtalk-connector': [['clientId', 'Client ID'], ['clientSecret', 'Client Secret']],
|
||||
mattermost: [['botToken', 'Bot Token'], ['baseUrl', 'Base URL']],
|
||||
'synology-chat': [['token', 'Token'], ['incomingUrl', 'Incoming URL']],
|
||||
clickclack: [['baseUrl', 'Base URL'], ['token', 'Token'], ['workspace', 'Workspace']],
|
||||
signal: [['account', 'Signal 账号']],
|
||||
}
|
||||
|
||||
@@ -3047,6 +3048,20 @@ export function buildMessagingPlatformFormValues(platform, saved = {}, options =
|
||||
return form
|
||||
}
|
||||
|
||||
if (storageKey === 'clickclack') {
|
||||
for (const key of ['name', 'baseUrl', 'token', 'workspace', 'botUserId', 'agentId', 'replyMode', 'model', 'systemPrompt', 'defaultTo']) {
|
||||
putSecretAwareFormValue(form, saved, key)
|
||||
}
|
||||
putBoolFormValue(form, saved, 'enabled')
|
||||
putBoolFormValue(form, saved, 'senderIsOwner')
|
||||
putCsvFormValue(form, saved, 'toolsAllow')
|
||||
putCsvFormValue(form, saved, 'allowFrom')
|
||||
for (const key of ['timeoutSeconds', 'reconnectMs']) {
|
||||
if (typeof saved[key] === 'number') form[key] = String(saved[key])
|
||||
}
|
||||
return form
|
||||
}
|
||||
|
||||
if (storageKey === 'synology-chat') {
|
||||
for (const key of ['token', 'incomingUrl', 'nasHost', 'webhookPath', 'botName']) {
|
||||
putSecretAwareFormValue(form, saved, key)
|
||||
@@ -3800,6 +3815,17 @@ function buildOpenClawMessagingPlatformEntry(platform, form, currentSaved = {})
|
||||
if (form.callbackPath) commands.callbackPath = form.callbackPath
|
||||
if (form.callbackUrl) commands.callbackUrl = form.callbackUrl
|
||||
if (Object.keys(commands).length) entry.commands = { ...(currentSaved?.commands || {}), ...commands }
|
||||
} else if (storageKey === 'clickclack') {
|
||||
entry.enabled = typeof form.enabled === 'boolean' ? form.enabled : true
|
||||
for (const key of ['name', 'baseUrl', 'token', 'workspace', 'botUserId', 'agentId', 'replyMode', 'model', 'systemPrompt', 'defaultTo']) {
|
||||
if (form[key]) entry[key] = form[key]
|
||||
}
|
||||
if (Array.isArray(form.toolsAllow) && form.toolsAllow.length) entry.toolsAllow = form.toolsAllow
|
||||
if (Array.isArray(form.allowFrom) && form.allowFrom.length) entry.allowFrom = form.allowFrom
|
||||
if (typeof form.senderIsOwner === 'boolean') entry.senderIsOwner = form.senderIsOwner
|
||||
for (const key of ['timeoutSeconds', 'reconnectMs']) {
|
||||
if (typeof form[key] === 'number') entry[key] = form[key]
|
||||
}
|
||||
} else if (storageKey === 'synology-chat') {
|
||||
for (const key of ['token', 'incomingUrl', 'nasHost', 'webhookPath', 'botName']) {
|
||||
if (form[key]) entry[key] = form[key]
|
||||
@@ -3841,7 +3867,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', 'imessage', 'whatsapp'].includes(storageKey)) {
|
||||
if (['zalo', 'zalouser', 'line', 'mattermost', 'clickclack', 'synology-chat', 'googlechat', 'msteams', 'imessage', 'whatsapp'].includes(storageKey)) {
|
||||
ensureMessagingPluginAllowed(cfg, storageKey)
|
||||
}
|
||||
return { entry, accountId: normalizedAccountId, storageKey }
|
||||
@@ -5311,7 +5337,7 @@ const handlers = {
|
||||
} else {
|
||||
setRootChannelEntry(entry)
|
||||
}
|
||||
} else if (['line', 'mattermost', 'synology-chat', 'googlechat', 'msteams', 'whatsapp'].includes(storageKey)) {
|
||||
} else if (['line', 'mattermost', 'clickclack', 'synology-chat', 'googlechat', 'msteams', 'whatsapp'].includes(storageKey)) {
|
||||
const built = buildOpenClawMessagingPlatformEntry(platform, form, currentSaved)
|
||||
applyMessagingPlatformEntry(cfg, storageKey, normalizedAccountId, built)
|
||||
ensureMessagingPluginAllowed(cfg, storageKey)
|
||||
@@ -5320,7 +5346,7 @@ const handlers = {
|
||||
preserveMessagingCredentialRefs(entry, form, currentSaved)
|
||||
}
|
||||
|
||||
if (platform !== 'qqbot' && platform !== 'feishu' && platform !== 'dingtalk' && platform !== 'dingtalk-connector' && !['line', 'mattermost', 'synology-chat', 'googlechat', 'msteams', 'whatsapp'].includes(storageKey)) {
|
||||
if (platform !== 'qqbot' && platform !== 'feishu' && platform !== 'dingtalk' && platform !== 'dingtalk-connector' && !['line', 'mattermost', 'clickclack', 'synology-chat', 'googlechat', 'msteams', 'whatsapp'].includes(storageKey)) {
|
||||
preserveMessagingCredentialRefs(entry, form, currentSaved)
|
||||
// 合并模式:保留用户通过 CLI 或手动编辑的自定义字段
|
||||
applyMessagingPlatformEntry(cfg, storageKey, normalizedAccountId, entry)
|
||||
@@ -5445,6 +5471,9 @@ const handlers = {
|
||||
if (platform === 'whatsapp') {
|
||||
return { valid: true, warnings: ['WhatsApp 使用扫码登录维护本地会话,无需在线校验 Bot Token;请通过「启动扫码登录」完成配对。'] }
|
||||
}
|
||||
if (platform === 'clickclack') {
|
||||
return { valid: true, warnings: ['ClickClack 面板已完成基础字段校验;实际连通性请通过 Gateway 启动日志或 openclaw channels status --probe 验证。'] }
|
||||
}
|
||||
if (platform === 'discord') {
|
||||
try {
|
||||
const resp = await fetch('https://discord.com/api/v10/users/@me', {
|
||||
|
||||
@@ -260,6 +260,11 @@ fn required_channel_credential_fields(
|
||||
"dingtalk-connector" => vec![("clientId", "Client ID"), ("clientSecret", "Client Secret")],
|
||||
"mattermost" => vec![("botToken", "Bot Token"), ("baseUrl", "Base URL")],
|
||||
"synology-chat" => vec![("token", "Token"), ("incomingUrl", "Incoming URL")],
|
||||
"clickclack" => vec![
|
||||
("baseUrl", "Base URL"),
|
||||
("token", "Token"),
|
||||
("workspace", "Workspace"),
|
||||
],
|
||||
"signal" => vec![("account", "Signal 账号")],
|
||||
"slack" => {
|
||||
let mode = form_string(form, "mode");
|
||||
@@ -905,12 +910,15 @@ fn normalize_messaging_platform_form(
|
||||
normalize_numeric_form_value(&mut normalized, "httpPort");
|
||||
normalize_numeric_form_value(&mut normalized, "webhookPort");
|
||||
normalize_numeric_form_value(&mut normalized, "feedbackReflectionCooldownMs");
|
||||
normalize_numeric_form_value(&mut normalized, "timeoutSeconds");
|
||||
normalize_numeric_form_value(&mut normalized, "reconnectMs");
|
||||
|
||||
for key in [
|
||||
"promptStarters",
|
||||
"delegatedAuthScopes",
|
||||
"attachmentRoots",
|
||||
"remoteAttachmentRoots",
|
||||
"toolsAllow",
|
||||
] {
|
||||
if normalized.contains_key(key) {
|
||||
let items = json_array_from_csv_value(normalized.get(key));
|
||||
@@ -940,6 +948,7 @@ fn normalize_messaging_platform_form(
|
||||
"coalesceSameSenderDms",
|
||||
"selfChatMode",
|
||||
"ackDirect",
|
||||
"senderIsOwner",
|
||||
] {
|
||||
if normalized.contains_key(key) {
|
||||
let value = match normalized.get(key) {
|
||||
@@ -1756,6 +1765,28 @@ pub async fn read_platform_config(
|
||||
insert_string_if_present(&mut form, commands, "callbackUrl");
|
||||
}
|
||||
}
|
||||
"clickclack" => {
|
||||
for key in [
|
||||
"name",
|
||||
"baseUrl",
|
||||
"token",
|
||||
"workspace",
|
||||
"botUserId",
|
||||
"agentId",
|
||||
"replyMode",
|
||||
"model",
|
||||
"systemPrompt",
|
||||
"defaultTo",
|
||||
] {
|
||||
insert_secret_aware_form_value(&mut form, &saved, key);
|
||||
}
|
||||
insert_bool_as_string(&mut form, &saved, "enabled");
|
||||
insert_bool_as_string(&mut form, &saved, "senderIsOwner");
|
||||
insert_array_as_csv(&mut form, &saved, "toolsAllow");
|
||||
insert_array_as_csv(&mut form, &saved, "allowFrom");
|
||||
insert_number_as_string(&mut form, &saved, "timeoutSeconds");
|
||||
insert_number_as_string(&mut form, &saved, "reconnectMs");
|
||||
}
|
||||
"synology-chat" => {
|
||||
for key in ["token", "incomingUrl", "nasHost", "webhookPath", "botName"] {
|
||||
insert_secret_aware_form_value(&mut form, &saved, key);
|
||||
@@ -2775,6 +2806,55 @@ pub async fn save_messaging_platform(
|
||||
)?;
|
||||
ensure_plugin_allowed(&mut cfg, "mattermost")?;
|
||||
}
|
||||
"clickclack" => {
|
||||
let base_url = form_string(form_obj, "baseUrl");
|
||||
let token = form_string(form_obj, "token");
|
||||
let workspace = form_string(form_obj, "workspace");
|
||||
if base_url.is_empty() {
|
||||
return Err("ClickClack Base URL 不能为空".into());
|
||||
}
|
||||
if token.is_empty() {
|
||||
return Err("ClickClack Token 不能为空".into());
|
||||
}
|
||||
if workspace.is_empty() {
|
||||
return Err("ClickClack Workspace 不能为空".into());
|
||||
}
|
||||
|
||||
let mut entry = Map::new();
|
||||
entry.insert("enabled".into(), Value::Bool(true));
|
||||
put_bool_value_if_present(&mut entry, "enabled", form_obj.get("enabled"));
|
||||
put_string(&mut entry, "baseUrl", base_url);
|
||||
put_string(&mut entry, "token", token);
|
||||
put_string(&mut entry, "workspace", workspace);
|
||||
for key in [
|
||||
"name",
|
||||
"botUserId",
|
||||
"agentId",
|
||||
"replyMode",
|
||||
"model",
|
||||
"systemPrompt",
|
||||
"defaultTo",
|
||||
] {
|
||||
put_string(&mut entry, key, form_string(form_obj, key));
|
||||
}
|
||||
put_array_from_form_value(&mut entry, "toolsAllow", form_obj.get("toolsAllow"));
|
||||
put_array_from_form_value(&mut entry, "allowFrom", form_obj.get("allowFrom"));
|
||||
put_bool_value_if_present(&mut entry, "senderIsOwner", form_obj.get("senderIsOwner"));
|
||||
put_number_value_if_present(
|
||||
&mut entry,
|
||||
"timeoutSeconds",
|
||||
form_obj.get("timeoutSeconds"),
|
||||
);
|
||||
put_number_value_if_present(&mut entry, "reconnectMs", form_obj.get("reconnectMs"));
|
||||
preserve_messaging_credential_refs(&mut entry, form_obj, ¤t_saved);
|
||||
merge_channel_entry_for_account(
|
||||
channels_map,
|
||||
&storage_key,
|
||||
account_id.as_deref(),
|
||||
entry,
|
||||
)?;
|
||||
ensure_plugin_allowed(&mut cfg, "clickclack")?;
|
||||
}
|
||||
"synology-chat" => {
|
||||
let token = form_string(form_obj, "token");
|
||||
let incoming_url = form_string(form_obj, "incomingUrl");
|
||||
@@ -3072,6 +3152,10 @@ pub async fn verify_bot_token(platform: String, form: Value) -> Result<Value, St
|
||||
"valid": true,
|
||||
"warnings": ["WhatsApp 使用扫码登录,无需在线校验凭证;请通过「启动扫码登录」完成配对"]
|
||||
})),
|
||||
"clickclack" => Ok(json!({
|
||||
"valid": true,
|
||||
"warnings": ["ClickClack 面板已完成基础字段校验;实际连通性请通过 Gateway 启动日志或 openclaw channels status --probe 验证"]
|
||||
})),
|
||||
_ => Ok(json!({
|
||||
"valid": true,
|
||||
"warnings": ["该平台暂不支持在线校验"]
|
||||
@@ -6144,6 +6228,82 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_clickclack_form_preserves_workspace_runtime_fields() {
|
||||
let form = json!({
|
||||
"enabled": "true",
|
||||
"baseUrl": "https://clickclack.example.com",
|
||||
"token": "clickclack-token",
|
||||
"workspace": "ops",
|
||||
"replyMode": "model",
|
||||
"timeoutSeconds": "120",
|
||||
"toolsAllow": "shell, browser.search",
|
||||
"senderIsOwner": "true",
|
||||
"defaultTo": "channel:ops",
|
||||
"allowFrom": "channel:ops, dm:alice",
|
||||
"reconnectMs": "2500"
|
||||
});
|
||||
let normalized =
|
||||
normalize_messaging_platform_form("clickclack", form.as_object().expect("object"));
|
||||
|
||||
assert_eq!(
|
||||
normalized.get("enabled").and_then(|v| v.as_bool()),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
normalized.get("timeoutSeconds").and_then(|v| v.as_f64()),
|
||||
Some(120.0)
|
||||
);
|
||||
assert_eq!(
|
||||
normalized.get("reconnectMs").and_then(|v| v.as_f64()),
|
||||
Some(2500.0)
|
||||
);
|
||||
assert_eq!(
|
||||
normalized.get("senderIsOwner").and_then(|v| v.as_bool()),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
normalized
|
||||
.get("toolsAllow")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|items| items.len()),
|
||||
Some(2)
|
||||
);
|
||||
assert_eq!(
|
||||
normalized
|
||||
.get("allowFrom")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|items| items.len()),
|
||||
Some(2)
|
||||
);
|
||||
assert!(channel_diagnosis_credentials_ready(
|
||||
"clickclack",
|
||||
&normalized
|
||||
));
|
||||
|
||||
let missing_workspace = json!({
|
||||
"baseUrl": "https://clickclack.example.com",
|
||||
"token": "clickclack-token"
|
||||
});
|
||||
let missing = normalize_messaging_platform_form(
|
||||
"clickclack",
|
||||
missing_workspace.as_object().expect("object"),
|
||||
);
|
||||
assert!(!channel_diagnosis_credentials_ready("clickclack", &missing));
|
||||
let diagnosis =
|
||||
build_openclaw_channel_diagnosis("clickclack", None, true, true, &missing, None, None);
|
||||
assert!(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("detail"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.contains("Workspace"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_form_readback_preserves_mention_policy_choice() {
|
||||
let saved = json!({
|
||||
|
||||
@@ -19,6 +19,7 @@ export const CHANNEL_LABELS = {
|
||||
line: 'LINE',
|
||||
nostr: 'Nostr',
|
||||
mattermost: 'Mattermost',
|
||||
clickclack: 'ClickClack',
|
||||
'openclaw-weixin': '微信',
|
||||
weixin: '微信',
|
||||
}
|
||||
|
||||
@@ -128,6 +128,22 @@ export default {
|
||||
mattermostNameMatchingHint: _('关闭时优先使用稳定 ID,避免同名用户或频道误匹配。', 'When disabled, prefer stable IDs to avoid wrong matches with duplicate users or channels.'),
|
||||
mattermostPrivateNetwork: _('允许内网地址', 'Allow Private Network'),
|
||||
mattermostPrivateNetworkHint: _('仅在 Mattermost 部署于可信内网时开启。', 'Enable only when Mattermost is deployed on a trusted private network.'),
|
||||
clickclackDesc: _('接入自托管 ClickClack 工作区,支持频道、线程和私信目标', 'Connect a self-hosted ClickClack workspace with channel, thread, and DM targets'),
|
||||
clickclackGuide1: _('确认 ClickClack 服务已部署并可被 Gateway 访问,复制工作区 Base URL', 'Make sure ClickClack is deployed and reachable by Gateway, then copy the workspace Base URL'),
|
||||
clickclackGuide2: _('准备访问 Token 与 Workspace 标识;多账号时建议填写账号标识,例如 work', 'Prepare the access Token and Workspace identifier; for multi-account setup, use an account id such as work'),
|
||||
clickclackGuide3: _('Default To 支持 <code>channel:<id></code>、<code>thread:<id></code>、<code>dm:<id></code>,例如 <code>channel:general</code>', 'Default To supports <code>channel:<id></code>, <code>thread:<id></code>, and <code>dm:<id></code>, e.g. <code>channel:general</code>'),
|
||||
clickclackGuide4: _('保存后面板会启用 bundled ClickClack 插件并重载 Gateway;连通性以 Gateway 日志或 channels status 为准', 'After saving, the panel enables the bundled ClickClack plugin and reloads Gateway; verify connectivity through Gateway logs or channels status'),
|
||||
clickclackGuideFooter: _('<div style="margin-top:8px;font-size:var(--font-size-xs);color:var(--text-tertiary)">ClickClack 最小配置需要 Base URL、Token 与 Workspace。</div>', '<div style="margin-top:8px;font-size:var(--font-size-xs);color:var(--text-tertiary)">ClickClack minimally requires Base URL, Token, and Workspace.</div>'),
|
||||
clickclackBaseUrlHint: _('填写 ClickClack 服务根地址,例如 https://clickclack.example.com。', 'Use the ClickClack service root URL, for example https://clickclack.example.com.'),
|
||||
clickclackTokenPh: _('ClickClack Access Token', 'ClickClack Access Token'),
|
||||
clickclackWorkspaceHint: _('工作区标识必须与 ClickClack 服务端配置一致。', 'Workspace must match the ClickClack server configuration.'),
|
||||
clickclackSystemPromptPh: _('可选,覆盖该渠道使用的系统提示词', 'Optional system prompt override for this channel'),
|
||||
clickclackTimeoutHint: _('请求超时时间,单位秒;上游允许 1 到 3600。', 'Request timeout in seconds; upstream allows 1 to 3600.'),
|
||||
clickclackToolsAllowHint: _('可选,逗号分隔允许调用的工具名。', 'Optional comma-separated tool names allowed for this channel.'),
|
||||
clickclackSenderIsOwner: _('发送者作为 Owner', 'Sender Is Owner'),
|
||||
clickclackSenderIsOwnerHint: _('开启后将 ClickClack 消息发送者视为 owner 身份,适合可信内部工作区。', 'Treat the ClickClack sender as owner when enabled; use only in trusted internal workspaces.'),
|
||||
clickclackDefaultToHint: _('默认发送目标,例如 channel:general、thread:123 或 dm:alice。', 'Default target, e.g. channel:general, thread:123, or dm:alice.'),
|
||||
clickclackAllowFromHint: _('可选,逗号分隔允许来源;留空按上游默认处理,* 表示允许全部。', 'Optional comma-separated allowed sources; leave empty for upstream defaults, * allows all.'),
|
||||
synologyChatDesc: _('接入群晖 Synology Chat,适合 NAS 内网团队协作', 'Connect Synology Chat for NAS-hosted team messaging'),
|
||||
synologyChatGuide1: _('在 Synology Chat 管理后台创建 Bot,并复制 Token', 'Create a bot in Synology Chat administration and copy its Token'),
|
||||
synologyChatGuide2: _('配置 Incoming Webhook 或机器人发消息 URL,填入 Incoming URL', 'Configure an Incoming Webhook or bot post URL, then paste it as Incoming URL'),
|
||||
|
||||
@@ -274,6 +274,42 @@ const PLATFORM_REGISTRY = {
|
||||
pluginRequired: '@openclaw/mattermost@latest',
|
||||
pluginId: 'mattermost',
|
||||
},
|
||||
clickclack: {
|
||||
label: 'ClickClack',
|
||||
iconName: 'message-square',
|
||||
desc: t('channels.clickclackDesc'),
|
||||
guide: [
|
||||
t('channels.clickclackGuide1'),
|
||||
t('channels.clickclackGuide2'),
|
||||
t('channels.clickclackGuide3'),
|
||||
t('channels.clickclackGuide4'),
|
||||
],
|
||||
guideFooter: t('channels.clickclackGuideFooter'),
|
||||
fields: [
|
||||
{ key: 'baseUrl', label: 'Base URL', placeholder: 'https://clickclack.example.com', required: true, hint: t('channels.clickclackBaseUrlHint') },
|
||||
{ key: 'token', label: 'Token', placeholder: t('channels.clickclackTokenPh'), secret: true, required: true },
|
||||
{ key: 'workspace', label: 'Workspace', placeholder: 'ops', required: true, hint: t('channels.clickclackWorkspaceHint') },
|
||||
{ key: 'name', label: t('channels.accountName'), placeholder: t('channels.optionalEg', { example: 'ops' }), required: false },
|
||||
{ key: 'botUserId', label: 'Bot User ID', placeholder: t('channels.optionalEg', { example: 'bot-1' }), required: false },
|
||||
{ key: 'agentId', label: 'Agent ID', placeholder: t('channels.optionalEg', { example: 'default' }), required: false },
|
||||
{ key: 'replyMode', label: 'Reply Mode', type: 'select', options: [
|
||||
{ value: '', label: t('channels.policyDefault') },
|
||||
{ value: 'agent', label: 'Agent' },
|
||||
{ value: 'model', label: 'Model' },
|
||||
], required: false },
|
||||
{ key: 'model', label: 'Model', placeholder: t('channels.optionalEg', { example: 'claude-sonnet-4-5' }), required: false },
|
||||
{ key: 'systemPrompt', label: 'System Prompt', placeholder: t('channels.clickclackSystemPromptPh'), multiline: true, required: false },
|
||||
{ key: 'timeoutSeconds', label: 'Timeout Seconds', placeholder: '120', required: false, hint: t('channels.clickclackTimeoutHint') },
|
||||
{ key: 'toolsAllow', label: 'Tools Allow', placeholder: 'shell, browser.search', required: false, hint: t('channels.clickclackToolsAllowHint') },
|
||||
{ key: 'senderIsOwner', label: t('channels.clickclackSenderIsOwner'), type: 'select', options: BOOLEAN_OPTIONS, required: false, hint: t('channels.clickclackSenderIsOwnerHint') },
|
||||
{ key: 'defaultTo', label: 'Default To', placeholder: 'channel:general', required: false, hint: t('channels.clickclackDefaultToHint') },
|
||||
{ key: 'allowFrom', label: 'Allow From', placeholder: '*, channel:general, dm:alice', required: false, hint: t('channels.clickclackAllowFromHint') },
|
||||
{ key: 'reconnectMs', label: 'Reconnect MS', placeholder: '1500', required: false },
|
||||
],
|
||||
configKey: 'clickclack',
|
||||
pairingChannel: 'clickclack',
|
||||
pluginId: 'clickclack',
|
||||
},
|
||||
'synology-chat': {
|
||||
label: 'Synology Chat',
|
||||
iconName: 'message-square',
|
||||
@@ -796,7 +832,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', 'signal']
|
||||
const MULTI_INSTANCE_PLATFORMS = ['telegram', 'discord', 'slack', 'feishu', 'dingtalk', 'dingtalk-connector', 'qqbot', 'zalo', 'zalouser', 'line', 'mattermost', 'clickclack', 'synology-chat', 'googlechat', 'signal']
|
||||
|
||||
function supportsMessagingMultiAccount(pid) {
|
||||
return MULTI_INSTANCE_PLATFORMS.includes(pid)
|
||||
|
||||
@@ -151,6 +151,104 @@ test('WhatsApp 读取会回显扫码运行字段且诊断不要求 Bot Token', (
|
||||
assert.match(diagnosis.checks.find(item => item.id === 'credentials')?.title || '', /扫码|会话/)
|
||||
})
|
||||
|
||||
test('ClickClack 渠道保存会写入自托管运行字段并启用插件', () => {
|
||||
const cfg = { channels: {} }
|
||||
|
||||
mergeOpenClawMessagingPlatformConfig(cfg, {
|
||||
platform: 'clickclack',
|
||||
accountId: 'work',
|
||||
form: {
|
||||
name: 'Ops Workspace',
|
||||
enabled: 'true',
|
||||
baseUrl: 'https://clickclack.example.com',
|
||||
token: 'clickclack-token',
|
||||
workspace: 'ops',
|
||||
botUserId: 'bot-1',
|
||||
agentId: 'agent-1',
|
||||
replyMode: 'model',
|
||||
model: 'claude-sonnet-4-5',
|
||||
systemPrompt: 'You are an ops assistant.',
|
||||
timeoutSeconds: '120',
|
||||
toolsAllow: 'shell, browser.search',
|
||||
senderIsOwner: 'true',
|
||||
defaultTo: 'channel:ops',
|
||||
allowFrom: 'channel:ops, dm:alice',
|
||||
reconnectMs: '2500',
|
||||
},
|
||||
})
|
||||
|
||||
const root = cfg.channels.clickclack
|
||||
const account = root.accounts.work
|
||||
assert.equal(root.defaultAccount, 'work')
|
||||
assert.equal(account.enabled, true)
|
||||
assert.equal(account.name, 'Ops Workspace')
|
||||
assert.equal(account.baseUrl, 'https://clickclack.example.com')
|
||||
assert.equal(account.token, 'clickclack-token')
|
||||
assert.equal(account.workspace, 'ops')
|
||||
assert.equal(account.botUserId, 'bot-1')
|
||||
assert.equal(account.agentId, 'agent-1')
|
||||
assert.equal(account.replyMode, 'model')
|
||||
assert.equal(account.model, 'claude-sonnet-4-5')
|
||||
assert.equal(account.systemPrompt, 'You are an ops assistant.')
|
||||
assert.equal(account.timeoutSeconds, 120)
|
||||
assert.deepEqual(account.toolsAllow, ['shell', 'browser.search'])
|
||||
assert.equal(account.senderIsOwner, true)
|
||||
assert.equal(account.defaultTo, 'channel:ops')
|
||||
assert.deepEqual(account.allowFrom, ['channel:ops', 'dm:alice'])
|
||||
assert.equal(account.reconnectMs, 2500)
|
||||
assert.equal(cfg.plugins.entries.clickclack.enabled, true)
|
||||
})
|
||||
|
||||
test('ClickClack 读取会回显运行字段且诊断要求 Base URL、Token 和 Workspace', () => {
|
||||
const values = buildMessagingPlatformFormValues('clickclack', {
|
||||
name: 'Ops Workspace',
|
||||
enabled: true,
|
||||
baseUrl: 'https://clickclack.example.com',
|
||||
token: 'clickclack-token',
|
||||
workspace: 'ops',
|
||||
botUserId: 'bot-1',
|
||||
agentId: 'agent-1',
|
||||
replyMode: 'agent',
|
||||
model: 'claude-sonnet-4-5',
|
||||
systemPrompt: 'You are an ops assistant.',
|
||||
timeoutSeconds: 90,
|
||||
toolsAllow: ['shell', 'browser.search'],
|
||||
senderIsOwner: false,
|
||||
defaultTo: 'channel:general',
|
||||
allowFrom: ['*'],
|
||||
reconnectMs: 1500,
|
||||
})
|
||||
const missingWorkspace = buildOpenClawChannelDiagnosis({
|
||||
platform: 'clickclack',
|
||||
configExists: true,
|
||||
channelEnabled: true,
|
||||
form: {
|
||||
baseUrl: 'https://clickclack.example.com',
|
||||
token: 'clickclack-token',
|
||||
},
|
||||
})
|
||||
const ready = buildOpenClawChannelDiagnosis({
|
||||
platform: 'clickclack',
|
||||
configExists: true,
|
||||
channelEnabled: true,
|
||||
form: values,
|
||||
})
|
||||
|
||||
assert.equal(values.enabled, 'true')
|
||||
assert.equal(values.baseUrl, 'https://clickclack.example.com')
|
||||
assert.equal(values.token, 'clickclack-token')
|
||||
assert.equal(values.workspace, 'ops')
|
||||
assert.equal(values.replyMode, 'agent')
|
||||
assert.equal(values.timeoutSeconds, '90')
|
||||
assert.equal(values.toolsAllow, 'shell, browser.search')
|
||||
assert.equal(values.senderIsOwner, 'false')
|
||||
assert.equal(values.allowFrom, '*')
|
||||
assert.equal(values.reconnectMs, '1500')
|
||||
assert.equal(missingWorkspace.checks.find(item => item.id === 'credentials')?.ok, false)
|
||||
assert.match(missingWorkspace.checks.find(item => item.id === 'credentials')?.detail || '', /Workspace/)
|
||||
assert.equal(ready.checks.find(item => item.id === 'credentials')?.ok, true)
|
||||
})
|
||||
|
||||
test('Signal 渠道保存会保留多账号和上游运行字段', () => {
|
||||
const cfg = { channels: {} }
|
||||
|
||||
|
||||
@@ -66,3 +66,27 @@ test('WhatsApp 渠道 UI 会恢复扫码登录和运行配置入口', () => {
|
||||
assert.match(whatsappBlock, /pluginRequired:\s*'@openclaw\/whatsapp@latest'/)
|
||||
assert.match(whatsappBlock, /pluginId:\s*'whatsapp'/)
|
||||
})
|
||||
|
||||
test('ClickClack 渠道 UI 会暴露自托管工作区配置字段', () => {
|
||||
const clickclackBlock = getRegistryBlock('clickclack')
|
||||
|
||||
for (const field of [
|
||||
'baseUrl',
|
||||
'token',
|
||||
'workspace',
|
||||
'botUserId',
|
||||
'agentId',
|
||||
'replyMode',
|
||||
'model',
|
||||
'systemPrompt',
|
||||
'timeoutSeconds',
|
||||
'toolsAllow',
|
||||
'senderIsOwner',
|
||||
'defaultTo',
|
||||
'allowFrom',
|
||||
'reconnectMs',
|
||||
]) {
|
||||
assert.match(clickclackBlock, new RegExp(`key:\\s*'${field}'`))
|
||||
}
|
||||
assert.match(clickclackBlock, /pluginId:\s*'clickclack'/)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user