mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-30 01:19:34 +00:00
Add WeChat typing indicators to the DM channel
This commit is contained in:
@@ -596,7 +596,7 @@ pub async fn start_server(
|
||||
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; \
|
||||
font-src https://fonts.gstatic.com; \
|
||||
connect-src 'self'; \
|
||||
img-src 'self' data:; \
|
||||
img-src 'self' data: https://liteapp.weixin.qq.com; \
|
||||
object-src 'none'; \
|
||||
frame-ancestors 'none'; \
|
||||
base-uri 'self'; \
|
||||
@@ -3010,6 +3010,35 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wechat_active_channel_does_not_require_pairing_status() -> Result<(), String> {
|
||||
let ext = InstalledExtension {
|
||||
name: "wechat".to_string(),
|
||||
kind: ExtensionKind::WasmChannel,
|
||||
display_name: Some("WeChat".to_string()),
|
||||
description: None,
|
||||
url: None,
|
||||
authenticated: true,
|
||||
active: true,
|
||||
tools: Vec::new(),
|
||||
needs_setup: true,
|
||||
has_auth: false,
|
||||
installed: true,
|
||||
activation_error: None,
|
||||
version: None,
|
||||
};
|
||||
|
||||
let status = classify_wasm_channel_activation(&ext, false, false);
|
||||
if status != Some(ExtensionActivationStatus::Active) {
|
||||
return Err(format!(
|
||||
"wechat should be active after QR login, got {:?}",
|
||||
status
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_channel_relay_activation_status_is_preserved() -> Result<(), String> {
|
||||
let relay = InstalledExtension {
|
||||
@@ -3242,7 +3271,7 @@ mod tests {
|
||||
crate::extensions::InteractiveLoginStartResult {
|
||||
session_id: "wechat-session-42".to_string(),
|
||||
status: "pending".to_string(),
|
||||
message: "Scan the QR code in WeChat to finish connecting.".to_string(),
|
||||
message: "Open the WeChat QR page to continue.".to_string(),
|
||||
qr_code_url: Some("https://qr.example/42".to_string()),
|
||||
instructions: Some(
|
||||
"Keep this window open while you scan and confirm on your phone."
|
||||
@@ -3605,6 +3634,10 @@ mod tests {
|
||||
csp_str.contains("object-src 'none'"),
|
||||
"CSP must contain object-src 'none'"
|
||||
);
|
||||
assert!(
|
||||
csp_str.contains("img-src 'self' data: https://liteapp.weixin.qq.com"),
|
||||
"CSP must allow WeChat QR images from liteapp.weixin.qq.com"
|
||||
);
|
||||
assert!(
|
||||
csp_str.contains("frame-ancestors 'none'"),
|
||||
"CSP must contain frame-ancestors 'none'"
|
||||
|
||||
@@ -3251,25 +3251,18 @@ function renderInteractiveLoginPanel() {
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.className = 'configure-verification-title';
|
||||
title.textContent = 'WeChat QR Login';
|
||||
title.textContent = 'Open WeChat QR Page';
|
||||
panel.appendChild(title);
|
||||
|
||||
const status = document.createElement('div');
|
||||
status.className = 'configure-verification-instructions';
|
||||
status.textContent = 'Generate a QR code to connect this channel.';
|
||||
status.textContent = 'The QR flow opens in a separate tab.';
|
||||
status.dataset.qrStatus = 'true';
|
||||
panel.appendChild(status);
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.className = 'configure-qr-image';
|
||||
img.alt = 'WeChat QR code';
|
||||
img.style.display = 'none';
|
||||
img.dataset.qrImage = 'true';
|
||||
panel.appendChild(img);
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.className = 'configure-verification-link';
|
||||
link.textContent = 'Open QR code in a new tab';
|
||||
link.textContent = 'Open QR Page';
|
||||
link.target = '_blank';
|
||||
link.rel = 'noreferrer noopener';
|
||||
link.style.display = 'none';
|
||||
@@ -3291,17 +3284,13 @@ function updateInteractiveLoginPanel(overlay, res) {
|
||||
const panel = getInteractiveLoginPanel(overlay);
|
||||
if (!panel) return;
|
||||
const status = panel.querySelector('[data-qr-status="true"]');
|
||||
const img = panel.querySelector('[data-qr-image="true"]');
|
||||
const link = panel.querySelector('[data-qr-link="true"]');
|
||||
|
||||
panel.style.display = '';
|
||||
if (status) {
|
||||
status.textContent = res.message || '';
|
||||
}
|
||||
|
||||
if (img && res.qr_code_url) {
|
||||
img.src = res.qr_code_url;
|
||||
img.style.display = '';
|
||||
status.textContent = res.status === 'refreshed'
|
||||
? 'The QR page was refreshed. Open it again if needed.'
|
||||
: 'The QR flow opens in a separate tab.';
|
||||
}
|
||||
|
||||
if (link && res.qr_code_url) {
|
||||
@@ -3322,7 +3311,7 @@ function setInteractiveLoginBusy(overlay, busy, label) {
|
||||
function startInteractiveLogin(name, overlay) {
|
||||
if (!overlay || !document.body.contains(overlay)) return;
|
||||
clearConfigureInlineError(overlay);
|
||||
setConfigureInlineStatus(overlay, 'Generating WeChat QR code...');
|
||||
setConfigureInlineStatus(overlay, 'Preparing WeChat QR page...');
|
||||
setInteractiveLoginBusy(overlay, true, 'Waiting for scan...');
|
||||
|
||||
apiFetch('/api/extensions/' + encodeURIComponent(name) + '/login/start', {
|
||||
@@ -3703,9 +3692,9 @@ function renderWasmChannelStepper(ext) {
|
||||
var status = ext.activation_status || 'installed';
|
||||
|
||||
var steps = [
|
||||
{ label: 'Installed', key: 'installed' },
|
||||
{ label: 'Configured', key: 'configured' },
|
||||
{ label: status === 'pairing' ? 'Awaiting Pairing' : 'Active', key: 'active' },
|
||||
{ label: I18n.t('status.installed'), key: 'installed' },
|
||||
{ label: I18n.t('status.configured'), key: 'configured' },
|
||||
{ label: status === 'pairing' ? I18n.t('status.pairingShort') : I18n.t('status.active'), key: 'active' },
|
||||
];
|
||||
|
||||
var reachedIdx;
|
||||
|
||||
@@ -54,7 +54,9 @@ I18n.register('en', {
|
||||
'status.restart': 'Restart',
|
||||
'status.active': 'Active',
|
||||
'status.installed': 'Installed',
|
||||
'status.configured': 'Configured',
|
||||
'status.awaitingPairing': 'Awaiting Pairing',
|
||||
'status.pairingShort': 'Pairing',
|
||||
|
||||
// Dashboard
|
||||
'dashboard.connections': 'Connections',
|
||||
|
||||
@@ -54,7 +54,9 @@ I18n.register('zh-CN', {
|
||||
'status.restart': '重启',
|
||||
'status.active': '已激活',
|
||||
'status.installed': '已安装',
|
||||
'status.configured': '已配置',
|
||||
'status.awaitingPairing': '等待配对',
|
||||
'status.pairingShort': '配对中',
|
||||
|
||||
// 仪表盘
|
||||
'dashboard.connections': '连接数',
|
||||
|
||||
@@ -2961,15 +2961,18 @@ body {
|
||||
/* WASM channel setup stepper */
|
||||
.ext-stepper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
gap: 0;
|
||||
margin: 8px 0 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.stepper-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.stepper-circle {
|
||||
@@ -2986,7 +2989,10 @@ body {
|
||||
|
||||
.stepper-label {
|
||||
font-size: var(--text-xs);
|
||||
white-space: nowrap;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
line-height: 1.25;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.stepper-step.completed .stepper-circle {
|
||||
@@ -3043,7 +3049,8 @@ body {
|
||||
height: 2px;
|
||||
background: var(--border);
|
||||
margin: 0 4px;
|
||||
flex-shrink: 0;
|
||||
flex: 0 0 20px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.stepper-connector.completed {
|
||||
@@ -3249,15 +3256,6 @@ body {
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.configure-qr-image {
|
||||
width: min(260px, 100%);
|
||||
align-self: center;
|
||||
border-radius: 10px;
|
||||
background: white;
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.configure-verification-title {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 600;
|
||||
@@ -3282,14 +3280,29 @@ body {
|
||||
}
|
||||
|
||||
.configure-verification-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: fit-content;
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--accent);
|
||||
background: var(--accent-subtle);
|
||||
color: var(--accent, var(--text-link, #4ea3ff));
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: background var(--transition-fast), transform 150ms var(--ease-spring);
|
||||
}
|
||||
|
||||
.configure-verification-link:hover {
|
||||
text-decoration: underline;
|
||||
background: var(--badge-sandbox-bg);
|
||||
transform: translateY(-1px);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.configure-verification-link:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.configure-inline-error {
|
||||
|
||||
@@ -466,7 +466,12 @@ pub fn classify_wasm_channel_activation(
|
||||
} else if !ext.authenticated {
|
||||
ExtensionActivationStatus::Installed
|
||||
} else if ext.active {
|
||||
if has_paired || has_owner_binding {
|
||||
// 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
|
||||
{
|
||||
ExtensionActivationStatus::Active
|
||||
} else {
|
||||
ExtensionActivationStatus::Pairing
|
||||
|
||||
@@ -7062,7 +7062,7 @@ mod tests {
|
||||
InteractiveLoginStartResult {
|
||||
session_id: "wechat-session-1".to_string(),
|
||||
status: "pending".to_string(),
|
||||
message: "Scan the QR code in WeChat to finish connecting.".to_string(),
|
||||
message: "Open the WeChat QR page to continue.".to_string(),
|
||||
qr_code_url: Some("https://qr.example/one".to_string()),
|
||||
instructions: Some(
|
||||
"Keep this window open while you scan and confirm on your phone."
|
||||
|
||||
@@ -135,7 +135,7 @@ fn build_pending_login(
|
||||
let result = InteractiveLoginStartResult {
|
||||
session_id,
|
||||
status: "pending".to_string(),
|
||||
message: "Scan the QR code in WeChat to finish connecting.".to_string(),
|
||||
message: "Open the WeChat QR page to continue.".to_string(),
|
||||
qr_code_url: Some(qr.qrcode_img_content),
|
||||
instructions: Some(
|
||||
"Keep this window open while you scan and confirm on your phone.".to_string(),
|
||||
|
||||
Reference in New Issue
Block a user