mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-31 00:29:24 +00:00
Fix WeChat extension status and login i18n
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user