mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-29 04:10:00 +08:00
fix(channels): add generic channel diagnostics
This commit is contained in:
@@ -2578,6 +2578,141 @@ function channelRootHasMessagingCredential(root) {
|
||||
return MESSAGING_CREDENTIAL_FIELDS.some(key => hasConfiguredMessagingValue(root[key]))
|
||||
}
|
||||
|
||||
const CHANNEL_DIAG_REQUIRED_FIELDS = {
|
||||
telegram: [['botToken', 'Bot Token']],
|
||||
discord: [['token', 'Bot Token']],
|
||||
feishu: [['appId', 'App ID'], ['appSecret', 'App Secret']],
|
||||
dingtalk: [['clientId', 'Client ID'], ['clientSecret', 'Client Secret']],
|
||||
'dingtalk-connector': [['clientId', 'Client ID'], ['clientSecret', 'Client Secret']],
|
||||
msteams: [['appId', 'App ID'], ['appPassword', 'App Password']],
|
||||
signal: [['account', 'Signal 账号']],
|
||||
}
|
||||
|
||||
function requiredChannelCredentialFields(platform, form = {}) {
|
||||
const storageKey = platformStorageKey(platform)
|
||||
if (storageKey === 'slack') {
|
||||
const mode = String(form.mode || 'socket').trim() || 'socket'
|
||||
return [
|
||||
['botToken', 'Bot Token'],
|
||||
mode === 'http' ? ['signingSecret', 'Signing Secret'] : ['appToken', 'App Token'],
|
||||
]
|
||||
}
|
||||
if (storageKey === 'matrix') {
|
||||
if (form.accessToken) return [['accessToken', 'Access Token']]
|
||||
return [['homeserver', 'Homeserver'], ['userId', 'User ID'], ['password', 'Password']]
|
||||
}
|
||||
return CHANNEL_DIAG_REQUIRED_FIELDS[storageKey] || []
|
||||
}
|
||||
|
||||
function channelDiagnosisCredentialsReady(platform, form = {}) {
|
||||
const requiredFields = requiredChannelCredentialFields(platform, form)
|
||||
if (requiredFields.length) {
|
||||
return requiredFields.every(([key]) => hasConfiguredMessagingValue(form?.[key]))
|
||||
}
|
||||
return channelRootHasMessagingCredential(form)
|
||||
}
|
||||
|
||||
function compactDiagnosticDetails(values = []) {
|
||||
return values.map(value => String(value || '').trim()).filter(Boolean).join(';')
|
||||
}
|
||||
|
||||
export function buildOpenClawChannelDiagnosis({
|
||||
platform,
|
||||
accountId = '',
|
||||
configExists = false,
|
||||
channelEnabled = true,
|
||||
form = {},
|
||||
verifyResult = null,
|
||||
verifyError = '',
|
||||
} = {}) {
|
||||
const storageKey = platformStorageKey(platform)
|
||||
const displayPlatform = platformListId(storageKey)
|
||||
const checks = []
|
||||
|
||||
checks.push({
|
||||
id: 'config_exists',
|
||||
ok: !!configExists,
|
||||
title: '渠道配置已保存',
|
||||
detail: configExists
|
||||
? `已读取 channels.${storageKey}${accountId ? `.accounts.${accountId}` : ''} 的配置。`
|
||||
: `未在 openclaw.json 中找到 ${displayPlatform} 渠道配置,请先在「渠道列表」接入并保存。`,
|
||||
})
|
||||
|
||||
checks.push({
|
||||
id: 'channel_enabled',
|
||||
ok: !!channelEnabled,
|
||||
title: '渠道已启用',
|
||||
detail: channelEnabled
|
||||
? '渠道未被显式禁用,Gateway 重启/重载后会尝试加载。'
|
||||
: `channels.${storageKey}.enabled 为 false,请先在渠道列表中启用该渠道。`,
|
||||
})
|
||||
|
||||
const requiredFields = requiredChannelCredentialFields(storageKey, form)
|
||||
const missing = requiredFields
|
||||
.filter(([key]) => !hasConfiguredMessagingValue(form?.[key]))
|
||||
.map(([, label]) => label)
|
||||
const hasAnyCredential = channelRootHasMessagingCredential(form)
|
||||
const credentialOk = requiredFields.length ? missing.length === 0 : hasAnyCredential
|
||||
checks.push({
|
||||
id: 'credentials',
|
||||
ok: credentialOk,
|
||||
title: '必要凭证字段',
|
||||
detail: credentialOk
|
||||
? (requiredFields.length
|
||||
? `已填写 ${requiredFields.map(([, label]) => label).join(' / ')}。`
|
||||
: '已检测到可用凭证字段。')
|
||||
: (missing.length
|
||||
? `缺少 ${missing.join(' / ')},请补齐后保存。`
|
||||
: '未检测到可用凭证字段,请检查渠道配置。'),
|
||||
})
|
||||
|
||||
if (verifyError) {
|
||||
checks.push({
|
||||
id: 'online_verify',
|
||||
ok: false,
|
||||
title: '平台在线校验',
|
||||
detail: verifyError,
|
||||
})
|
||||
} else if (verifyResult) {
|
||||
const valid = verifyResult.valid === true
|
||||
const errors = Array.isArray(verifyResult.errors) ? verifyResult.errors : []
|
||||
const warnings = Array.isArray(verifyResult.warnings) ? verifyResult.warnings : []
|
||||
const details = Array.isArray(verifyResult.details) ? verifyResult.details : []
|
||||
checks.push({
|
||||
id: 'online_verify',
|
||||
ok: valid || (!valid && warnings.length > 0 && errors.length === 0),
|
||||
title: '平台在线校验',
|
||||
detail: valid
|
||||
? (compactDiagnosticDetails(details) || '平台 API 已接受当前凭证。')
|
||||
: (compactDiagnosticDetails(errors) || compactDiagnosticDetails(warnings) || '该平台暂不支持在线校验。'),
|
||||
})
|
||||
} else {
|
||||
checks.push({
|
||||
id: 'online_verify',
|
||||
ok: true,
|
||||
title: '平台在线校验',
|
||||
detail: '未执行在线校验,仅完成本地配置检查。',
|
||||
})
|
||||
}
|
||||
|
||||
const failed = checks.filter(check => !check.ok)
|
||||
return {
|
||||
ok: failed.length === 0,
|
||||
overallReady: failed.length === 0,
|
||||
platform: displayPlatform,
|
||||
accountId: accountId || null,
|
||||
checks,
|
||||
userHints: failed.length
|
||||
? [
|
||||
'先修复未通过的检查项,保存渠道后重启或重载 Gateway。',
|
||||
'在线校验只能证明平台凭证可用;群聊白名单、机器人邀请和平台回调仍需在对应平台控制台确认。',
|
||||
]
|
||||
: [
|
||||
'配置侧检查已通过。若仍收不到消息,请确认 Gateway 已重启、机器人已加入目标会话,并检查 Gateway 日志。',
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function preserveMessagingCredentialRefs(entry, form, current) {
|
||||
delete entry.__secretRefs
|
||||
for (const key of MESSAGING_CREDENTIAL_FIELDS) {
|
||||
@@ -9666,8 +9801,46 @@ const handlers = {
|
||||
// 静默返回未安装即可,UI 会显示"未安装"
|
||||
return { installed: false, version: null, plugin: null }
|
||||
},
|
||||
diagnose_channel() {
|
||||
return { ok: false, error: 'Web 模式暂未实现渠道诊断,请使用桌面客户端' }
|
||||
async diagnose_channel({ platform, accountId } = {}) {
|
||||
if (!platform || !String(platform).trim()) throw new Error('platform 不能为空')
|
||||
const platformId = String(platform).trim()
|
||||
const normalizedAccountId = typeof accountId === 'string' ? accountId.trim() : ''
|
||||
const storageKey = platformStorageKey(platformId)
|
||||
const cfg = readOpenclawConfigOptional()
|
||||
const channelRoot = cfg.channels?.[storageKey]
|
||||
const saved = handlers.read_platform_config({ platform: platformId, accountId: normalizedAccountId || null })
|
||||
const form = saved?.values || {}
|
||||
const configExists = !!saved?.exists
|
||||
const channelEnabled = !channelRoot || channelRoot.enabled !== false
|
||||
const credentialsReady = channelDiagnosisCredentialsReady(platformId, form)
|
||||
let verifyResult = null
|
||||
let verifyError = ''
|
||||
|
||||
if (configExists && credentialsReady) {
|
||||
try {
|
||||
verifyResult = await handlers.verify_bot_token({ platform: platformId, form })
|
||||
} catch (e) {
|
||||
verifyError = e?.message || String(e)
|
||||
}
|
||||
}
|
||||
|
||||
const result = buildOpenClawChannelDiagnosis({
|
||||
platform: platformId,
|
||||
accountId: normalizedAccountId,
|
||||
configExists,
|
||||
channelEnabled,
|
||||
form,
|
||||
verifyResult,
|
||||
verifyError,
|
||||
})
|
||||
if (storageKey === 'qqbot') {
|
||||
result.userHints = [
|
||||
'Web 模式已完成配置级检查;QQ 插件、Gateway TCP 和 chatCompletions 深度诊断需要在桌面客户端执行。',
|
||||
...(result.userHints || []),
|
||||
]
|
||||
result.faqUrl = 'https://q.qq.com/qqbot/openclaw/faq.html'
|
||||
}
|
||||
return result
|
||||
},
|
||||
run_channel_action() {
|
||||
throw new Error('Web 模式暂未实现渠道操作,请使用桌面客户端')
|
||||
|
||||
@@ -200,6 +200,220 @@ fn value_has_messaging_credential(value: &Value) -> bool {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn required_channel_credential_fields(
|
||||
platform: &str,
|
||||
form: &Map<String, Value>,
|
||||
) -> Vec<(&'static str, &'static str)> {
|
||||
match platform_storage_key(platform) {
|
||||
"telegram" => vec![("botToken", "Bot Token")],
|
||||
"discord" => vec![("token", "Bot Token")],
|
||||
"feishu" => vec![("appId", "App ID"), ("appSecret", "App Secret")],
|
||||
"dingtalk-connector" => vec![("clientId", "Client ID"), ("clientSecret", "Client Secret")],
|
||||
"msteams" => vec![("appId", "App ID"), ("appPassword", "App Password")],
|
||||
"signal" => vec![("account", "Signal 账号")],
|
||||
"slack" => {
|
||||
let mode = form_string(form, "mode");
|
||||
vec![
|
||||
("botToken", "Bot Token"),
|
||||
if mode == "http" {
|
||||
("signingSecret", "Signing Secret")
|
||||
} else {
|
||||
("appToken", "App Token")
|
||||
},
|
||||
]
|
||||
}
|
||||
"matrix" => {
|
||||
if has_configured_messaging_value(form.get("accessToken")) {
|
||||
vec![("accessToken", "Access Token")]
|
||||
} else {
|
||||
vec![
|
||||
("homeserver", "Homeserver"),
|
||||
("userId", "User ID"),
|
||||
("password", "Password"),
|
||||
]
|
||||
}
|
||||
}
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn channel_diagnosis_credentials_ready(platform: &str, form: &Map<String, Value>) -> bool {
|
||||
let required_fields = required_channel_credential_fields(platform, form);
|
||||
if required_fields.is_empty() {
|
||||
return channel_root_has_messaging_credential(form);
|
||||
}
|
||||
required_fields
|
||||
.iter()
|
||||
.all(|(key, _)| has_configured_messaging_value(form.get(*key)))
|
||||
}
|
||||
|
||||
fn json_string_list(value: Option<&Value>) -> Vec<String> {
|
||||
value
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|item| item.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|item| !item.is_empty())
|
||||
.map(str::to_string)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn compact_diagnostic_details(values: &[String]) -> String {
|
||||
values
|
||||
.iter()
|
||||
.map(|value| value.trim())
|
||||
.filter(|value| !value.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(";")
|
||||
}
|
||||
|
||||
fn build_openclaw_channel_diagnosis(
|
||||
platform: &str,
|
||||
account_id: Option<&str>,
|
||||
config_exists: bool,
|
||||
channel_enabled: bool,
|
||||
form: &Map<String, Value>,
|
||||
verify_result: Option<Value>,
|
||||
verify_error: Option<String>,
|
||||
) -> Value {
|
||||
let storage_key = platform_storage_key(platform);
|
||||
let display_platform = platform_list_id(storage_key);
|
||||
let account_id = account_id.map(str::trim).filter(|id| !id.is_empty());
|
||||
let mut checks = Vec::new();
|
||||
|
||||
checks.push(json!({
|
||||
"id": "config_exists",
|
||||
"ok": config_exists,
|
||||
"title": "渠道配置已保存",
|
||||
"detail": if config_exists {
|
||||
format!(
|
||||
"已读取 channels.{}{} 的配置。",
|
||||
storage_key,
|
||||
account_id.map(|id| format!(".accounts.{}", id)).unwrap_or_default()
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"未在 openclaw.json 中找到 {} 渠道配置,请先在「渠道列表」接入并保存。",
|
||||
display_platform
|
||||
)
|
||||
}
|
||||
}));
|
||||
|
||||
checks.push(json!({
|
||||
"id": "channel_enabled",
|
||||
"ok": channel_enabled,
|
||||
"title": "渠道已启用",
|
||||
"detail": if channel_enabled {
|
||||
"渠道未被显式禁用,Gateway 重启/重载后会尝试加载。".to_string()
|
||||
} else {
|
||||
format!("channels.{}.enabled 为 false,请先在渠道列表中启用该渠道。", storage_key)
|
||||
}
|
||||
}));
|
||||
|
||||
let required_fields = required_channel_credential_fields(storage_key, form);
|
||||
let missing: Vec<&str> = required_fields
|
||||
.iter()
|
||||
.filter(|(key, _)| !has_configured_messaging_value(form.get(*key)))
|
||||
.map(|(_, label)| *label)
|
||||
.collect();
|
||||
let credential_ok = if required_fields.is_empty() {
|
||||
channel_root_has_messaging_credential(form)
|
||||
} else {
|
||||
missing.is_empty()
|
||||
};
|
||||
let required_labels = required_fields
|
||||
.iter()
|
||||
.map(|(_, label)| *label)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" / ");
|
||||
checks.push(json!({
|
||||
"id": "credentials",
|
||||
"ok": credential_ok,
|
||||
"title": "必要凭证字段",
|
||||
"detail": if credential_ok {
|
||||
if required_fields.is_empty() {
|
||||
"已检测到可用凭证字段。".to_string()
|
||||
} else {
|
||||
format!("已填写 {}。", required_labels)
|
||||
}
|
||||
} else if missing.is_empty() {
|
||||
"未检测到可用凭证字段,请检查渠道配置。".to_string()
|
||||
} else {
|
||||
format!("缺少 {},请补齐后保存。", missing.join(" / "))
|
||||
}
|
||||
}));
|
||||
|
||||
if let Some(error) = verify_error.filter(|error| !error.trim().is_empty()) {
|
||||
checks.push(json!({
|
||||
"id": "online_verify",
|
||||
"ok": false,
|
||||
"title": "平台在线校验",
|
||||
"detail": error
|
||||
}));
|
||||
} else if let Some(result) = verify_result {
|
||||
let valid = result.get("valid").and_then(|v| v.as_bool()) == Some(true);
|
||||
let errors = json_string_list(result.get("errors"));
|
||||
let warnings = json_string_list(result.get("warnings"));
|
||||
let details = json_string_list(result.get("details"));
|
||||
let verify_ok = valid || (!warnings.is_empty() && errors.is_empty());
|
||||
checks.push(json!({
|
||||
"id": "online_verify",
|
||||
"ok": verify_ok,
|
||||
"title": "平台在线校验",
|
||||
"detail": if valid {
|
||||
let detail = compact_diagnostic_details(&details);
|
||||
if detail.is_empty() {
|
||||
"平台 API 已接受当前凭证。".to_string()
|
||||
} else {
|
||||
detail
|
||||
}
|
||||
} else {
|
||||
let detail = compact_diagnostic_details(&errors);
|
||||
if detail.is_empty() {
|
||||
let warning_detail = compact_diagnostic_details(&warnings);
|
||||
if warning_detail.is_empty() {
|
||||
"该平台暂不支持在线校验。".to_string()
|
||||
} else {
|
||||
warning_detail
|
||||
}
|
||||
} else {
|
||||
detail
|
||||
}
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
checks.push(json!({
|
||||
"id": "online_verify",
|
||||
"ok": true,
|
||||
"title": "平台在线校验",
|
||||
"detail": "未执行在线校验,仅完成本地配置检查。"
|
||||
}));
|
||||
}
|
||||
|
||||
let failed_count = checks
|
||||
.iter()
|
||||
.filter(|check| check.get("ok").and_then(|v| v.as_bool()) != Some(true))
|
||||
.count();
|
||||
json!({
|
||||
"ok": failed_count == 0,
|
||||
"overallReady": failed_count == 0,
|
||||
"platform": display_platform,
|
||||
"accountId": account_id,
|
||||
"checks": checks,
|
||||
"userHints": if failed_count == 0 {
|
||||
vec!["配置侧检查已通过。若仍收不到消息,请确认 Gateway 已重启、机器人已加入目标会话,并检查 Gateway 日志。"]
|
||||
} else {
|
||||
vec![
|
||||
"先修复未通过的检查项,保存渠道后重启或重载 Gateway。",
|
||||
"在线校验只能证明平台凭证可用;群聊白名单、机器人邀请和平台回调仍需在对应平台控制台确认。",
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn insert_bool_as_string(form: &mut Map<String, Value>, source: &Value, key: &str) {
|
||||
if let Some(v) = source.get(key).and_then(|v| v.as_bool()) {
|
||||
form.insert(
|
||||
@@ -2308,13 +2522,55 @@ pub async fn diagnose_channel(
|
||||
platform: String,
|
||||
account_id: Option<String>,
|
||||
) -> Result<Value, String> {
|
||||
match platform.as_str() {
|
||||
"qqbot" => diagnose_qqbot_channel(account_id).await,
|
||||
_ => Err(format!(
|
||||
"暂不支持平台「{}」的深度诊断(当前仅实现 qqbot)",
|
||||
platform
|
||||
)),
|
||||
let platform = platform.trim().to_string();
|
||||
if platform.is_empty() {
|
||||
return Err("platform 不能为空".into());
|
||||
}
|
||||
if platform == "qqbot" {
|
||||
return diagnose_qqbot_channel(account_id).await;
|
||||
}
|
||||
|
||||
let cfg = super::config::load_openclaw_json().unwrap_or_else(|_| json!({}));
|
||||
let storage_key = platform_storage_key(&platform);
|
||||
let normalized_account_id = account_id
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|id| !id.is_empty());
|
||||
let channel_root = cfg.get("channels").and_then(|c| c.get(storage_key));
|
||||
let channel_enabled = channel_root
|
||||
.and_then(|node| node.get("enabled"))
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(true);
|
||||
let saved =
|
||||
read_platform_config(platform.clone(), normalized_account_id.map(str::to_string)).await?;
|
||||
let config_exists = saved
|
||||
.get("exists")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
let form = saved
|
||||
.get("values")
|
||||
.and_then(|v| v.as_object())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let credentials_ready = channel_diagnosis_credentials_ready(&platform, &form);
|
||||
let (verify_result, verify_error) = if config_exists && credentials_ready {
|
||||
match verify_bot_token(platform.clone(), Value::Object(form.clone())).await {
|
||||
Ok(result) => (Some(result), None),
|
||||
Err(error) => (None, Some(error)),
|
||||
}
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
Ok(build_openclaw_channel_diagnosis(
|
||||
&platform,
|
||||
normalized_account_id,
|
||||
config_exists,
|
||||
channel_enabled,
|
||||
&form,
|
||||
verify_result,
|
||||
verify_error,
|
||||
))
|
||||
}
|
||||
|
||||
/// 一键修复 QQ 插件:未安装则安装官方包并重启 Gateway;已安装则补齐 plugins.allow / entries 并重载 Gateway。
|
||||
|
||||
@@ -219,6 +219,12 @@ export default {
|
||||
qqRepairBtn: _('一键修复', 'One-Click Repair', '一鍵修复'),
|
||||
qqFaqBtn: _('打开 QQ OpenClaw 常见问题', 'Open QQ OpenClaw FAQ', '開啟 QQ OpenClaw 常见問題'),
|
||||
qqDiagTitle: _('QQ 联通诊断', 'QQ Connectivity Diagnosis', 'QQ 聯通诊斷'),
|
||||
channelDiagTitle: _('{platform} 联通诊断', '{platform} Connectivity Diagnosis', '{platform} 聯通诊斷'),
|
||||
channelDiagAllPassed: _('配置侧检查已通过', 'Configuration checks passed', '設定側檢查已通過'),
|
||||
channelDiagHasFailed: _('存在需要处理的检查项', 'Some checks need attention', '存在需要處理的檢查項'),
|
||||
channelDiagPassed: _('通过', 'Passed', '通過'),
|
||||
channelDiagNeedsAction: _('需处理', 'Needs action', '需處理'),
|
||||
channelDiagNoChecks: _('暂无诊断结果', 'No diagnosis results yet', '暫無诊斷結果'),
|
||||
notes: _('说明', 'Notes', '說明'),
|
||||
processing: _('处理中...', 'Processing...', '處理中...'),
|
||||
repairDone: _('修复完成', 'Repair complete'),
|
||||
|
||||
@@ -1476,6 +1476,39 @@ function showQqDiagnoseModal(result, options = {}) {
|
||||
})
|
||||
}
|
||||
|
||||
function showChannelDiagnoseModal(result, options = {}) {
|
||||
const platformName = options.platformName || result?.platform || t('channels.channel')
|
||||
const checks = Array.isArray(result?.checks) ? result.checks : []
|
||||
const summaryOk = !!result?.overallReady
|
||||
const summary = summaryOk
|
||||
? `<div style="background:var(--success-muted);color:var(--success);padding:10px 14px;border-radius:var(--radius-md);margin-bottom:12px;font-size:var(--font-size-sm);line-height:1.5">${icon('check', 14)} ${t('channels.channelDiagAllPassed')}</div>`
|
||||
: `<div style="background:var(--warning-muted);color:var(--warning);padding:10px 14px;border-radius:var(--radius-md);margin-bottom:12px;font-size:var(--font-size-sm);line-height:1.5">${icon('alert-triangle', 14)} ${t('channels.channelDiagHasFailed')}</div>`
|
||||
const list = checks.map(c => {
|
||||
const ok = !!c.ok
|
||||
const tone = ok ? 'var(--success)' : 'var(--error)'
|
||||
const bg = ok ? 'var(--success-muted)' : 'var(--error-muted, rgba(220,38,38,0.1))'
|
||||
const label = ok ? t('channels.channelDiagPassed') : t('channels.channelDiagNeedsAction')
|
||||
return `<div style="border:1px solid ${bg};border-left:3px solid ${tone};padding:10px 12px;margin-bottom:8px;background:var(--bg-tertiary);border-radius:var(--radius-md)">
|
||||
<div style="display:flex;align-items:center;gap:8px;font-weight:600;color:var(--text-primary)">
|
||||
<span style="color:${tone};min-width:18px">${ok ? icon('check', 14) : icon('x', 14)}</span>
|
||||
<span>${escapeAttr(c.title || '')}</span>
|
||||
<span style="margin-left:auto;font-size:var(--font-size-xs);font-weight:600;color:${tone};background:${bg};padding:2px 8px;border-radius:999px;white-space:nowrap">${label}</span>
|
||||
</div>
|
||||
<div style="font-size:var(--font-size-sm);color:var(--text-secondary);margin-top:6px;line-height:1.55;white-space:pre-wrap">${escapeAttr(c.detail || '')}</div>
|
||||
</div>`
|
||||
}).join('')
|
||||
const hints = (result?.userHints || []).map(h =>
|
||||
`<li style="margin-bottom:8px;line-height:1.5">${escapeAttr(h)}</li>`
|
||||
).join('')
|
||||
const empty = `<div class="form-hint" style="padding:12px 0">${t('channels.channelDiagNoChecks')}</div>`
|
||||
|
||||
showContentModal({
|
||||
title: t('channels.channelDiagTitle', { platform: platformName }),
|
||||
content: `${summary}<div style="max-height:min(52vh,420px);overflow-y:auto;margin-bottom:12px;margin-top:12px">${list || empty}</div><div style="font-weight:600;margin-bottom:8px;font-size:var(--font-size-sm)">${t('channels.notes')}</div><ul style="padding-left:18px;font-size:var(--font-size-sm);color:var(--text-secondary);margin:0">${hints}</ul>`,
|
||||
width: 540,
|
||||
})
|
||||
}
|
||||
|
||||
async function runChannelTestForBinding(binding, btnEl) {
|
||||
const match = binding?.match || {}
|
||||
const channel = match.channel
|
||||
@@ -1489,30 +1522,18 @@ async function runChannelTestForBinding(binding, btnEl) {
|
||||
const prevHtml = btnEl?.innerHTML
|
||||
if (btnEl) {
|
||||
btnEl.disabled = true
|
||||
btnEl.textContent = channel === 'qqbot' ? t('channels.diagnosing') : t('channels.testing')
|
||||
btnEl.textContent = t('channels.diagnosing')
|
||||
}
|
||||
try {
|
||||
const result = await api.diagnoseChannel(platformId, accountId)
|
||||
if (channel === 'qqbot') {
|
||||
const result = await api.diagnoseChannel('qqbot', accountId)
|
||||
showQqDiagnoseModal(result, { accountId })
|
||||
return
|
||||
}
|
||||
const res = await api.readPlatformConfig(platformId, accountId)
|
||||
if (!res?.exists) {
|
||||
toast(t('channels.noCredentialsFound'), 'warning')
|
||||
return
|
||||
}
|
||||
const form = res.values || {}
|
||||
const out = await api.verifyBotToken(platformId, form)
|
||||
if (out.valid) {
|
||||
const details = (out.details || []).join(' · ')
|
||||
toast(`${t('channels.testPassed')}${details ? ': ' + details : ''}`, 'success')
|
||||
} else {
|
||||
const errs = (out.errors || [t('channels.verifyFailed')]).join('; ')
|
||||
toast(t('channels.testFailed') + ': ' + errs, 'error')
|
||||
}
|
||||
const platformName = PLATFORM_REGISTRY[platformId]?.label || CHANNEL_LABELS[platformId] || platformId
|
||||
showChannelDiagnoseModal(result, { platformName, accountId })
|
||||
} catch (e) {
|
||||
toast(humanizeError(e, channel === 'qqbot' ? t('channels.diagFailed') : t('channels.testFailed')), 'error')
|
||||
toast(humanizeError(e, t('channels.diagFailed')), 'error')
|
||||
} finally {
|
||||
if (btnEl) {
|
||||
btnEl.disabled = false
|
||||
|
||||
@@ -2,6 +2,7 @@ import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import {
|
||||
buildOpenClawChannelDiagnosis,
|
||||
buildMessagingPlatformFormValues,
|
||||
listPlatformAccounts,
|
||||
mergeOpenClawMessagingPlatformConfig,
|
||||
@@ -286,6 +287,84 @@ test('OpenClaw 渠道保存带账号标识时会写入 accounts 而不是覆盖
|
||||
assert.equal(cfg.channels.slack.accounts['team-a'].appToken, 'team-app')
|
||||
})
|
||||
|
||||
test('通用渠道诊断会指出 Telegram 缺少 Bot Token', () => {
|
||||
const result = buildOpenClawChannelDiagnosis({
|
||||
platform: 'telegram',
|
||||
configExists: true,
|
||||
channelEnabled: true,
|
||||
form: {
|
||||
dmPolicy: 'pairing',
|
||||
groupPolicy: 'allowlist',
|
||||
},
|
||||
})
|
||||
|
||||
assert.equal(result.ok, false)
|
||||
assert.equal(result.overallReady, false)
|
||||
assert.equal(result.checks.find(item => item.id === 'credentials')?.ok, false)
|
||||
assert.match(result.checks.find(item => item.id === 'credentials')?.detail || '', /Bot Token/)
|
||||
})
|
||||
|
||||
test('通用渠道诊断在缺少渠道配置时不会误报渠道已禁用', () => {
|
||||
const result = buildOpenClawChannelDiagnosis({
|
||||
platform: 'telegram',
|
||||
configExists: false,
|
||||
channelEnabled: true,
|
||||
form: {},
|
||||
})
|
||||
|
||||
assert.equal(result.ok, false)
|
||||
assert.equal(result.checks.find(item => item.id === 'config_exists')?.ok, false)
|
||||
assert.equal(result.checks.find(item => item.id === 'channel_enabled')?.ok, true)
|
||||
assert.match(result.checks.find(item => item.id === 'channel_enabled')?.detail || '', /未被显式禁用/)
|
||||
})
|
||||
|
||||
test('通用渠道诊断会按 Slack 模式检查动态必填凭证', () => {
|
||||
const socketResult = buildOpenClawChannelDiagnosis({
|
||||
platform: 'slack',
|
||||
configExists: true,
|
||||
channelEnabled: true,
|
||||
form: {
|
||||
mode: 'socket',
|
||||
botToken: 'xoxb-token',
|
||||
},
|
||||
})
|
||||
const httpResult = buildOpenClawChannelDiagnosis({
|
||||
platform: 'slack',
|
||||
configExists: true,
|
||||
channelEnabled: true,
|
||||
form: {
|
||||
mode: 'http',
|
||||
botToken: 'xoxb-token',
|
||||
signingSecret: 'secret',
|
||||
},
|
||||
})
|
||||
|
||||
assert.equal(socketResult.ok, false)
|
||||
assert.match(socketResult.checks.find(item => item.id === 'credentials')?.detail || '', /App Token/)
|
||||
assert.equal(httpResult.checks.find(item => item.id === 'credentials')?.ok, true)
|
||||
})
|
||||
|
||||
test('通用渠道诊断会识别钉钉 Client ID 和 Client Secret', () => {
|
||||
const result = buildOpenClawChannelDiagnosis({
|
||||
platform: 'dingtalk',
|
||||
configExists: true,
|
||||
channelEnabled: true,
|
||||
form: {
|
||||
clientId: 'ding-app-key',
|
||||
clientSecret: 'ding-secret',
|
||||
},
|
||||
verifyResult: {
|
||||
valid: true,
|
||||
details: ['已通过 accessToken 接口校验'],
|
||||
},
|
||||
})
|
||||
|
||||
assert.equal(result.ok, true)
|
||||
assert.equal(result.overallReady, true)
|
||||
assert.equal(result.checks.find(item => item.id === 'credentials')?.ok, true)
|
||||
assert.equal(result.checks.find(item => item.id === 'online_verify')?.ok, true)
|
||||
})
|
||||
|
||||
test('Discord 渠道保存会保留运行时需要的 applicationId', () => {
|
||||
const cfg = { channels: {} }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user