mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
Fix WeChat extension status and login i18n
This commit is contained in:
@@ -139,6 +139,13 @@ impl ChannelCapabilitiesFile {
|
||||
serde_json::to_string(&self.config).unwrap_or_else(|_| "{}".to_string())
|
||||
}
|
||||
|
||||
/// Whether this channel declares owner/pairing gating in its config.
|
||||
pub fn requires_binding(&self) -> bool {
|
||||
["owner_id", "dm_policy", "allow_from"]
|
||||
.iter()
|
||||
.any(|key| self.config.contains_key(*key))
|
||||
}
|
||||
|
||||
/// Get the webhook secret header name for this channel.
|
||||
///
|
||||
/// Returns the configured header name from capabilities, or a sensible default.
|
||||
@@ -569,6 +576,34 @@ mod tests {
|
||||
assert_eq!(caps.workspace_prefix, "integrations/custom/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_requires_binding_detects_dm_owner_fields() {
|
||||
let telegram = ChannelCapabilitiesFile::from_json(
|
||||
r#"{
|
||||
"name": "telegram",
|
||||
"config": {
|
||||
"owner_id": null,
|
||||
"dm_policy": "pairing",
|
||||
"allow_from": []
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(telegram.requires_binding());
|
||||
|
||||
let wechat = ChannelCapabilitiesFile::from_json(
|
||||
r#"{
|
||||
"name": "wechat",
|
||||
"config": {
|
||||
"base_url": "https://ilinkai.weixin.qq.com",
|
||||
"bot_type": "3"
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!wechat.requires_binding());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_emit_rate_limit() {
|
||||
let json = r#"{
|
||||
|
||||
@@ -47,6 +47,7 @@ pub async fn extensions_list_handler(
|
||||
&ext,
|
||||
has_paired,
|
||||
owner_bound_channels.contains(&ext.name),
|
||||
ext.requires_binding,
|
||||
)
|
||||
} else if ext.kind == crate::extensions::ExtensionKind::ChannelRelay {
|
||||
Some(if ext.active {
|
||||
|
||||
@@ -2050,6 +2050,7 @@ async fn extensions_list_handler(
|
||||
&ext,
|
||||
has_paired,
|
||||
owner_bound_channels.contains(&ext.name),
|
||||
ext.requires_binding,
|
||||
)
|
||||
} else if ext.kind == crate::extensions::ExtensionKind::ChannelRelay {
|
||||
Some(if ext.active {
|
||||
@@ -2986,12 +2987,13 @@ mod tests {
|
||||
tools: Vec::new(),
|
||||
needs_setup: true,
|
||||
has_auth: false,
|
||||
requires_binding: true,
|
||||
installed: true,
|
||||
activation_error: None,
|
||||
version: None,
|
||||
};
|
||||
|
||||
let owner_bound = classify_wasm_channel_activation(&ext, false, true);
|
||||
let owner_bound = classify_wasm_channel_activation(&ext, false, true, ext.requires_binding);
|
||||
if owner_bound != Some(ExtensionActivationStatus::Active) {
|
||||
return Err(format!(
|
||||
"owner-bound channel should be active, got {:?}",
|
||||
@@ -2999,7 +3001,7 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
let unbound = classify_wasm_channel_activation(&ext, false, false);
|
||||
let unbound = classify_wasm_channel_activation(&ext, false, false, ext.requires_binding);
|
||||
if unbound != Some(ExtensionActivationStatus::Pairing) {
|
||||
return Err(format!(
|
||||
"unbound channel should be pairing, got {:?}",
|
||||
@@ -3023,12 +3025,13 @@ mod tests {
|
||||
tools: Vec::new(),
|
||||
needs_setup: true,
|
||||
has_auth: false,
|
||||
requires_binding: false,
|
||||
installed: true,
|
||||
activation_error: None,
|
||||
version: None,
|
||||
};
|
||||
|
||||
let status = classify_wasm_channel_activation(&ext, false, false);
|
||||
let status = classify_wasm_channel_activation(&ext, false, false, ext.requires_binding);
|
||||
if status != Some(ExtensionActivationStatus::Active) {
|
||||
return Err(format!(
|
||||
"wechat should be active after QR login, got {:?}",
|
||||
@@ -3052,13 +3055,14 @@ mod tests {
|
||||
tools: Vec::new(),
|
||||
needs_setup: true,
|
||||
has_auth: false,
|
||||
requires_binding: false,
|
||||
installed: true,
|
||||
activation_error: None,
|
||||
version: None,
|
||||
};
|
||||
|
||||
let status = if relay.kind == crate::extensions::ExtensionKind::WasmChannel {
|
||||
classify_wasm_channel_activation(&relay, false, false)
|
||||
classify_wasm_channel_activation(&relay, false, false, relay.requires_binding)
|
||||
} else if relay.kind == crate::extensions::ExtensionKind::ChannelRelay {
|
||||
Some(if relay.active {
|
||||
ExtensionActivationStatus::Active
|
||||
|
||||
@@ -3058,12 +3058,12 @@ function showConfigureModal(name) {
|
||||
const setupFields = Array.isArray(setup.fields) ? setup.fields : [];
|
||||
const interactiveLogin = setup.interactive_login || null;
|
||||
if (secrets.length === 0 && setupFields.length === 0 && !interactiveLogin) {
|
||||
showToast('No configuration needed for ' + name, 'info');
|
||||
showToast(I18n.t('extensions.noConfigNeeded', { name: name }), 'info');
|
||||
return;
|
||||
}
|
||||
renderConfigureModal(name, secrets, setupFields, interactiveLogin);
|
||||
})
|
||||
.catch((err) => showToast('Failed to load setup: ' + err.message, 'error'));
|
||||
.catch((err) => showToast(I18n.t('error.loadFailed', { message: err.message }), 'error'));
|
||||
}
|
||||
|
||||
function renderConfigureModal(name, secrets, setupFields, interactiveLogin) {
|
||||
@@ -3092,10 +3092,10 @@ function renderConfigureModal(name, secrets, setupFields, interactiveLogin) {
|
||||
modal.appendChild(hint);
|
||||
}
|
||||
|
||||
if (interactiveLogin && interactiveLogin.instructions) {
|
||||
if (interactiveLogin) {
|
||||
const hint = document.createElement('div');
|
||||
hint.className = 'configure-hint';
|
||||
hint.textContent = interactiveLogin.instructions;
|
||||
hint.textContent = interactiveLoginHintText(name, interactiveLogin);
|
||||
modal.appendChild(hint);
|
||||
}
|
||||
|
||||
@@ -3193,7 +3193,7 @@ function renderConfigureModal(name, secrets, setupFields, interactiveLogin) {
|
||||
}
|
||||
|
||||
if (interactiveLogin) {
|
||||
modal.appendChild(renderInteractiveLoginPanel());
|
||||
modal.appendChild(renderInteractiveLoginPanel(name));
|
||||
}
|
||||
|
||||
const error = document.createElement('div');
|
||||
@@ -3220,7 +3220,8 @@ function renderConfigureModal(name, secrets, setupFields, interactiveLogin) {
|
||||
if (interactiveLogin) {
|
||||
const loginBtn = document.createElement('button');
|
||||
loginBtn.className = 'btn-ext activate';
|
||||
loginBtn.textContent = interactiveLogin.button_label || 'Connect';
|
||||
loginBtn.dataset.defaultLabel = interactiveLoginDefaultLabel(name, interactiveLogin);
|
||||
loginBtn.textContent = loginBtn.dataset.defaultLabel;
|
||||
loginBtn.dataset.interactiveLogin = 'true';
|
||||
loginBtn.addEventListener('click', () => startInteractiveLogin(name, overlay));
|
||||
actions.appendChild(loginBtn);
|
||||
@@ -3244,25 +3245,27 @@ function renderConfigureModal(name, secrets, setupFields, interactiveLogin) {
|
||||
}
|
||||
}
|
||||
|
||||
function renderInteractiveLoginPanel() {
|
||||
function renderInteractiveLoginPanel(name) {
|
||||
const panel = document.createElement('div');
|
||||
panel.className = 'configure-qr-login';
|
||||
panel.style.display = 'none';
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.className = 'configure-verification-title';
|
||||
title.textContent = 'Open WeChat QR Page';
|
||||
title.textContent =
|
||||
name === 'wechat' ? I18n.t('config.wechatQrTitle') : I18n.t('auth.connect');
|
||||
panel.appendChild(title);
|
||||
|
||||
const status = document.createElement('div');
|
||||
status.className = 'configure-verification-instructions';
|
||||
status.textContent = 'The QR flow opens in a separate tab.';
|
||||
status.textContent = interactiveLoginStatusText(name, null);
|
||||
status.dataset.qrStatus = 'true';
|
||||
panel.appendChild(status);
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.className = 'configure-verification-link';
|
||||
link.textContent = 'Open QR Page';
|
||||
link.textContent =
|
||||
name === 'wechat' ? I18n.t('config.wechatQrOpen') : I18n.t('auth.connect');
|
||||
link.target = '_blank';
|
||||
link.rel = 'noreferrer noopener';
|
||||
link.style.display = 'none';
|
||||
@@ -3272,6 +3275,41 @@ function renderInteractiveLoginPanel() {
|
||||
return panel;
|
||||
}
|
||||
|
||||
function interactiveLoginHintText(name, interactiveLogin) {
|
||||
if (name === 'wechat') return I18n.t('config.wechatHint');
|
||||
return (interactiveLogin && interactiveLogin.instructions) || '';
|
||||
}
|
||||
|
||||
function interactiveLoginDefaultLabel(name, interactiveLogin) {
|
||||
if (name === 'wechat') return I18n.t('config.wechatConnect');
|
||||
return (interactiveLogin && interactiveLogin.button_label) || I18n.t('auth.connect');
|
||||
}
|
||||
|
||||
function interactiveLoginWaitingLabel(name) {
|
||||
if (name === 'wechat') return I18n.t('config.wechatWaiting');
|
||||
return I18n.t('status.connecting');
|
||||
}
|
||||
|
||||
function interactiveLoginStatusText(name, res) {
|
||||
if (name !== 'wechat') return (res && res.message) || '';
|
||||
if (!res) return I18n.t('config.wechatQrIntro');
|
||||
|
||||
switch (res.status) {
|
||||
case 'pending':
|
||||
return res.qr_code_url ? I18n.t('config.wechatQrReady') : I18n.t('config.wechatQrWaiting');
|
||||
case 'scanned':
|
||||
return I18n.t('config.wechatQrScanned');
|
||||
case 'refreshed':
|
||||
return I18n.t('config.wechatQrRefreshed');
|
||||
case 'succeeded':
|
||||
return I18n.t('config.wechatConnected');
|
||||
case 'failed':
|
||||
return res.message || I18n.t('config.wechatQrFailed');
|
||||
default:
|
||||
return res.message || I18n.t('config.wechatQrIntro');
|
||||
}
|
||||
}
|
||||
|
||||
function getInteractiveLoginButton(overlay) {
|
||||
return overlay && overlay.querySelector('.configure-actions button[data-interactive-login="true"]');
|
||||
}
|
||||
@@ -3283,14 +3321,17 @@ function getInteractiveLoginPanel(overlay) {
|
||||
function updateInteractiveLoginPanel(overlay, res) {
|
||||
const panel = getInteractiveLoginPanel(overlay);
|
||||
if (!panel) return;
|
||||
const name = overlay && overlay.dataset ? overlay.dataset.extensionName : '';
|
||||
const status = panel.querySelector('[data-qr-status="true"]');
|
||||
const link = panel.querySelector('[data-qr-link="true"]');
|
||||
|
||||
panel.style.display = '';
|
||||
if (status) {
|
||||
status.textContent = res.status === 'refreshed'
|
||||
? 'The QR page was refreshed. Open it again if needed.'
|
||||
: 'The QR flow opens in a separate tab.';
|
||||
if (name === 'wechat' && res.status === 'refreshed') {
|
||||
status.textContent = I18n.t('config.wechatQrRefreshedHint');
|
||||
} else {
|
||||
status.textContent = interactiveLoginStatusText(name, res);
|
||||
}
|
||||
}
|
||||
|
||||
if (link && res.qr_code_url) {
|
||||
@@ -3303,16 +3344,17 @@ function setInteractiveLoginBusy(overlay, busy, label) {
|
||||
const loginBtn = getInteractiveLoginButton(overlay);
|
||||
if (!loginBtn) return;
|
||||
loginBtn.disabled = !!busy;
|
||||
if (label) {
|
||||
loginBtn.textContent = label;
|
||||
}
|
||||
loginBtn.textContent = label || loginBtn.dataset.defaultLabel || I18n.t('auth.connect');
|
||||
}
|
||||
|
||||
function startInteractiveLogin(name, overlay) {
|
||||
if (!overlay || !document.body.contains(overlay)) return;
|
||||
clearConfigureInlineError(overlay);
|
||||
setConfigureInlineStatus(overlay, 'Preparing WeChat QR page...');
|
||||
setInteractiveLoginBusy(overlay, true, 'Waiting for scan...');
|
||||
setConfigureInlineStatus(
|
||||
overlay,
|
||||
name === 'wechat' ? I18n.t('config.wechatPreparingQr') : I18n.t('status.connecting'),
|
||||
);
|
||||
setInteractiveLoginBusy(overlay, true, interactiveLoginWaitingLabel(name));
|
||||
|
||||
apiFetch('/api/extensions/' + encodeURIComponent(name) + '/login/start', {
|
||||
method: 'POST',
|
||||
@@ -3321,21 +3363,27 @@ function startInteractiveLogin(name, overlay) {
|
||||
.then((res) => {
|
||||
if (!overlay || !document.body.contains(overlay)) return;
|
||||
if (!res.success || !res.session_id) {
|
||||
setInteractiveLoginBusy(overlay, false, 'Connect WeChat');
|
||||
setConfigureInlineError(overlay, res.message || 'Failed to start interactive login');
|
||||
setInteractiveLoginBusy(overlay, false);
|
||||
setConfigureInlineError(
|
||||
overlay,
|
||||
res.message || I18n.t('config.interactiveLoginStartFailed'),
|
||||
);
|
||||
setConfigureInlineStatus(overlay, '');
|
||||
return;
|
||||
}
|
||||
|
||||
overlay.dataset.interactiveLoginSessionId = res.session_id;
|
||||
updateInteractiveLoginPanel(overlay, res);
|
||||
setConfigureInlineStatus(overlay, res.message || '');
|
||||
setConfigureInlineStatus(overlay, interactiveLoginStatusText(name, res));
|
||||
pollInteractiveLogin(name, overlay, res.session_id);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!overlay || !document.body.contains(overlay)) return;
|
||||
setInteractiveLoginBusy(overlay, false, 'Connect WeChat');
|
||||
setConfigureInlineError(overlay, err.message || 'Failed to start interactive login');
|
||||
setInteractiveLoginBusy(overlay, false);
|
||||
setConfigureInlineError(
|
||||
overlay,
|
||||
err.message || I18n.t('config.interactiveLoginStartFailed'),
|
||||
);
|
||||
setConfigureInlineStatus(overlay, '');
|
||||
});
|
||||
}
|
||||
@@ -3352,16 +3400,12 @@ function pollInteractiveLogin(name, overlay, sessionId) {
|
||||
if (!overlay || !document.body.contains(overlay)) return;
|
||||
if (overlay.dataset.interactiveLoginSessionId !== sessionId) return;
|
||||
|
||||
if (res.qr_code_url) {
|
||||
updateInteractiveLoginPanel(overlay, res);
|
||||
}
|
||||
if (res.message) {
|
||||
setConfigureInlineStatus(overlay, res.message);
|
||||
}
|
||||
updateInteractiveLoginPanel(overlay, res);
|
||||
setConfigureInlineStatus(overlay, interactiveLoginStatusText(name, res));
|
||||
|
||||
if (res.status === 'pending' || res.status === 'scanned' || res.status === 'refreshed') {
|
||||
if (res.status === 'refreshed') {
|
||||
setInteractiveLoginBusy(overlay, true, 'Waiting for scan...');
|
||||
setInteractiveLoginBusy(overlay, true, interactiveLoginWaitingLabel(name));
|
||||
}
|
||||
window.setTimeout(function() {
|
||||
pollInteractiveLogin(name, overlay, sessionId);
|
||||
@@ -3371,20 +3415,20 @@ function pollInteractiveLogin(name, overlay, sessionId) {
|
||||
|
||||
if (res.success && res.activated) {
|
||||
closeConfigureModal(name);
|
||||
showToast(res.message || (name + ' connected successfully'), 'success');
|
||||
showToast(res.message || I18n.t('config.connectedSuccess', { name: name }), 'success');
|
||||
refreshCurrentSettingsTab();
|
||||
return;
|
||||
}
|
||||
|
||||
setInteractiveLoginBusy(overlay, false, 'Connect WeChat');
|
||||
setConfigureInlineError(overlay, res.message || 'Interactive login failed');
|
||||
setInteractiveLoginBusy(overlay, false);
|
||||
setConfigureInlineError(overlay, res.message || I18n.t('config.interactiveLoginFailed'));
|
||||
setConfigureInlineStatus(overlay, '');
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!overlay || !document.body.contains(overlay)) return;
|
||||
if (overlay.dataset.interactiveLoginSessionId !== sessionId) return;
|
||||
setInteractiveLoginBusy(overlay, false, 'Connect WeChat');
|
||||
setConfigureInlineError(overlay, err.message || 'Interactive login failed');
|
||||
setInteractiveLoginBusy(overlay, false);
|
||||
setConfigureInlineError(overlay, err.message || I18n.t('config.interactiveLoginFailed'));
|
||||
setConfigureInlineStatus(overlay, '');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -361,6 +361,23 @@ I18n.register('en', {
|
||||
'config.telegramStartOver': 'Start over',
|
||||
'config.telegramStartOverHint': 'Telegram verification did not complete. Click Start over to generate a new code and try again.',
|
||||
'config.telegramOpenBot': 'Open bot in Telegram',
|
||||
'config.wechatHint': 'Open the WeChat QR page in a new tab, then scan and confirm in WeChat.',
|
||||
'config.wechatConnect': 'Open QR Page',
|
||||
'config.wechatWaiting': 'Waiting for scan...',
|
||||
'config.wechatPreparingQr': 'Preparing WeChat QR page...',
|
||||
'config.wechatQrTitle': 'Open WeChat QR Page',
|
||||
'config.wechatQrOpen': 'Open QR Page',
|
||||
'config.wechatQrIntro': 'The QR flow opens in a separate tab.',
|
||||
'config.wechatQrReady': 'QR page is ready. Open it in a new tab, then scan and confirm in WeChat.',
|
||||
'config.wechatQrWaiting': 'Preparing the WeChat QR page...',
|
||||
'config.wechatQrScanned': 'QR scanned. Confirm the login in WeChat.',
|
||||
'config.wechatQrRefreshed': 'QR page refreshed.',
|
||||
'config.wechatQrRefreshedHint': 'The previous QR page expired. Open the new page and scan again.',
|
||||
'config.wechatConnected': 'WeChat connected.',
|
||||
'config.wechatQrFailed': 'WeChat connection failed.',
|
||||
'config.interactiveLoginStartFailed': 'Failed to start interactive login',
|
||||
'config.interactiveLoginFailed': 'Interactive login failed',
|
||||
'config.connectedSuccess': '{name} connected successfully',
|
||||
'config.optional': ' (optional)',
|
||||
'config.alreadySet': '(already set — leave empty to keep)',
|
||||
'config.alreadyConfigured': 'Already configured',
|
||||
|
||||
@@ -360,6 +360,24 @@ I18n.register('zh-CN', {
|
||||
'config.telegramCommandLabel': '请在 Telegram 中发送:',
|
||||
'config.telegramStartOver': '重新开始',
|
||||
'config.telegramStartOverHint': 'Telegram 验证未完成。点击“重新开始”以生成新的验证码并重试。',
|
||||
'config.telegramOpenBot': '在 Telegram 中打开机器人',
|
||||
'config.wechatHint': '在新标签页打开微信扫码页,然后在微信里扫码并确认。',
|
||||
'config.wechatConnect': '打开扫码页',
|
||||
'config.wechatWaiting': '等待扫码中...',
|
||||
'config.wechatPreparingQr': '正在准备微信扫码页...',
|
||||
'config.wechatQrTitle': '打开微信扫码页',
|
||||
'config.wechatQrOpen': '打开扫码页',
|
||||
'config.wechatQrIntro': '扫码流程会在新标签页中打开。',
|
||||
'config.wechatQrReady': '扫码页已就绪。请在新标签页打开后,用微信扫码并确认。',
|
||||
'config.wechatQrWaiting': '正在准备微信扫码页...',
|
||||
'config.wechatQrScanned': '已扫码,请在微信中确认登录。',
|
||||
'config.wechatQrRefreshed': '扫码页已刷新。',
|
||||
'config.wechatQrRefreshedHint': '之前的扫码页已过期,请打开新页面重新扫码。',
|
||||
'config.wechatConnected': '微信已连接。',
|
||||
'config.wechatQrFailed': '微信连接失败。',
|
||||
'config.interactiveLoginStartFailed': '启动交互式登录失败',
|
||||
'config.interactiveLoginFailed': '交互式登录失败',
|
||||
'config.connectedSuccess': '{name} 连接成功',
|
||||
'config.optional': '(可选)',
|
||||
'config.alreadySet': '(已设置 — 留空以保持不变)',
|
||||
'config.alreadyConfigured': '已配置',
|
||||
|
||||
@@ -456,6 +456,7 @@ pub fn classify_wasm_channel_activation(
|
||||
ext: &crate::extensions::InstalledExtension,
|
||||
has_paired: bool,
|
||||
has_owner_binding: bool,
|
||||
requires_binding: bool,
|
||||
) -> Option<ExtensionActivationStatus> {
|
||||
if ext.kind != crate::extensions::ExtensionKind::WasmChannel {
|
||||
return None;
|
||||
@@ -466,12 +467,7 @@ pub fn classify_wasm_channel_activation(
|
||||
} else if !ext.authenticated {
|
||||
ExtensionActivationStatus::Installed
|
||||
} else if ext.active {
|
||||
// WeChat QR login already proves channel ownership and does not have a
|
||||
// separate owner-binding/pairing phase like Telegram DM verification.
|
||||
if ext.name == crate::extensions::wechat_login::WECHAT_CHANNEL_NAME
|
||||
|| has_paired
|
||||
|| has_owner_binding
|
||||
{
|
||||
if !requires_binding || has_paired || has_owner_binding {
|
||||
ExtensionActivationStatus::Active
|
||||
} else {
|
||||
ExtensionActivationStatus::Pairing
|
||||
|
||||
+23
-13
@@ -1344,6 +1344,7 @@ impl ExtensionManager {
|
||||
tools,
|
||||
needs_setup: false,
|
||||
has_auth: false,
|
||||
requires_binding: false,
|
||||
installed: true,
|
||||
activation_error: None,
|
||||
version: None,
|
||||
@@ -1395,6 +1396,7 @@ impl ExtensionManager {
|
||||
tools: if active { vec![name] } else { Vec::new() },
|
||||
needs_setup: auth_state == ToolAuthState::NeedsSetup,
|
||||
has_auth: auth_state != ToolAuthState::NoAuth,
|
||||
requires_binding: false,
|
||||
installed: true,
|
||||
activation_error: None,
|
||||
version,
|
||||
@@ -1424,20 +1426,25 @@ impl ExtensionManager {
|
||||
.get_with_kind(&name, Some(ExtensionKind::WasmChannel))
|
||||
.await;
|
||||
let display_name = registry_entry.as_ref().map(|e| e.display_name.clone());
|
||||
let version = if let Some(ref cap_path) = discovered.capabilities_path {
|
||||
tokio::fs::read(cap_path)
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|bytes| {
|
||||
crate::channels::wasm::ChannelCapabilitiesFile::from_bytes(
|
||||
&bytes,
|
||||
)
|
||||
let (version, requires_binding) =
|
||||
if let Some(ref cap_path) = discovered.capabilities_path {
|
||||
tokio::fs::read(cap_path)
|
||||
.await
|
||||
.ok()
|
||||
})
|
||||
.and_then(|cap| cap.version)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
.and_then(|bytes| {
|
||||
crate::channels::wasm::ChannelCapabilitiesFile::from_bytes(
|
||||
&bytes,
|
||||
)
|
||||
.ok()
|
||||
})
|
||||
.map(|cap| {
|
||||
let requires_binding = cap.requires_binding();
|
||||
(cap.version, requires_binding)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
.unwrap_or((None, false));
|
||||
let version =
|
||||
version.or_else(|| registry_entry.and_then(|e| e.version.clone()));
|
||||
extensions.push(InstalledExtension {
|
||||
@@ -1451,6 +1458,7 @@ impl ExtensionManager {
|
||||
tools: Vec::new(),
|
||||
needs_setup: auth_state == ToolAuthState::NeedsSetup,
|
||||
has_auth: auth_state != ToolAuthState::NoAuth,
|
||||
requires_binding,
|
||||
installed: true,
|
||||
activation_error,
|
||||
version,
|
||||
@@ -1489,6 +1497,7 @@ impl ExtensionManager {
|
||||
tools: Vec::new(),
|
||||
needs_setup: false,
|
||||
has_auth: true,
|
||||
requires_binding: false,
|
||||
installed: true,
|
||||
activation_error,
|
||||
version: None,
|
||||
@@ -1523,6 +1532,7 @@ impl ExtensionManager {
|
||||
tools: Vec::new(),
|
||||
needs_setup: false,
|
||||
has_auth: false,
|
||||
requires_binding: false,
|
||||
installed: false,
|
||||
activation_error: None,
|
||||
version: entry.version,
|
||||
|
||||
@@ -553,6 +553,10 @@ pub struct InstalledExtension {
|
||||
/// Whether this extension has an auth configuration (OAuth or manual token).
|
||||
#[serde(default)]
|
||||
pub has_auth: bool,
|
||||
/// Whether this extension still needs owner binding / pairing before it should
|
||||
/// be treated as fully active in the UI.
|
||||
#[serde(default)]
|
||||
pub requires_binding: bool,
|
||||
/// Whether this extension is installed locally (false = available in registry but not installed).
|
||||
#[serde(default = "default_true")]
|
||||
pub installed: bool,
|
||||
@@ -1003,6 +1007,7 @@ mod tests {
|
||||
tools: vec!["send_email".to_string(), "read_inbox".to_string()],
|
||||
needs_setup: true,
|
||||
has_auth: true,
|
||||
requires_binding: false,
|
||||
installed: false,
|
||||
activation_error: Some("token expired".to_string()),
|
||||
version: None,
|
||||
|
||||
Reference in New Issue
Block a user